Glyph and three other Twisted developers visited me yesterday. All is well. We're behind in reporting -- I have a variety of trips and other activities coming up, but I am still very much planning to act on what we discussed. (And no, they didn't convince me to add Twisted to the stdlib. :-)<br>

<br>--Guido<br><div class="gmail_extra"><br><br><div class="gmail_quote">On Wed, Nov 7, 2012 at 1:11 AM, Devin Jeanpierre <span dir="ltr"><<a href="mailto:jeanpierreda@gmail.com" target="_blank">jeanpierreda@gmail.com</a>></span> wrote:<br>

<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">It's been a week, and nobody has responded to Glyph's email. I don't<br>
think I know enough to agree or disagree with what he said, but it was<br>
well-written and it looked important. Also, Glyph has a lot of<br>
experience with this sort of thing, and it would be a shame if he was<br>
discouraged by the lack of response. We can't really expect people to<br>
contribute if their opinions are ignored.<br>
<br>
Can relevant people please take another look at his post?<br>
<span class="HOEnZb"><font color="#888888"><br>
-- Devin<br>
</font></span><div class="im HOEnZb"><br>
On Wed, Oct 31, 2012 at 6:10 AM, Glyph <<a href="mailto:glyph@twistedmatrix.com">glyph@twistedmatrix.com</a>> wrote:<br>
</div><div class="HOEnZb"><div class="h5">> Finally getting around to this one...<br>
><br>
> I am sorry if I'm repeating any criticism that has already been rehashed in<br>
> this thread.  There is really a deluge of mail here and I can't keep up with<br>
> it.  I've skimmed some of it and avoided or noted things that I did see<br>
> mentioned, but I figured I should write up something before next week.<br>
><br>
> To make a long story short, my main points here are:<br>
><br>
> I think tulip unfortunately has a lot of the problems I tried to describe in<br>
> earlier messages,<br>
> it would be really great if we could have a core I/O interface that we could<br>
> use for interoperability with Twisted before bolting a requirement for<br>
> coroutine trampolines on to everything,<br>
> twisted-style protocol/transport separation is really important and this<br>
> should not neglect it.  As I've tried to illustrate in previous messages, an<br>
> API where applications have to call send() or recv() is just not going to<br>
> behave intuitively in edge cases or perform well,<br>
> I know it's a prototype, but this isn't such an unexplored area that it<br>
> should be developed without TDD: all this code should both have tests and<br>
> provide testing support to show how applications that use it can be tested<br>
> the scheduler module needs some example implementation of something like<br>
> Twisted's gatherResults for me to critique its expressiveness; it looks like<br>
> it might be missing something in the area of one task coordinating multiple<br>
> others but I can't tell<br>
><br>
><br>
> On Oct 28, 2012, at 4:52 PM, Guido van Rossum <guido at <a href="http://python.org" target="_blank">python.org</a>> wrote:<br>
><br>
> The pollster has a very simple API: add_reader(fd, callback, *args),<br>
><br>
> add_writer(<ditto>), remove_reader(fd), remove_writer(fd), and<br>
> poll(timeout) -> list of events. (fd means file descriptor.) There's<br>
> also pollable() which just checks if there are any fds registered. My<br>
> implementation requires fd to be an int, but that could easily be<br>
> extended to support other types of event sources.<br>
><br>
><br>
> I don't see how that is.  All of the mechanisms I would leverage within<br>
> Twisted to support other event sources are missing (e.g.: abstract<br>
> interfaces for those event sources).  Are you saying that a totally<br>
> different pollster could just accept a different type to add_reader, and not<br>
> an integer?  If so, how would application code know how to construct<br>
> something else.<br>
><br>
> I'm not super happy that I have parallel reader/writer APIs, but passing a<br>
> separate read/write flag didn't come out any more elegant, and I don't<br>
> foresee other operation types (though I may be wrong).<br>
><br>
><br>
> add_reader and add_writer is an important internal layer of the API for<br>
> UNIX-like operating systems, but the design here is fundamentally flawed in<br>
> that application code (e.g. echosvr.py) needs to import concrete<br>
> socket-handling classes like SocketTransport and BufferedReader in order to<br>
> synthesize a transport.  These classes might need to vary their behavior<br>
> significantly between platforms, and application code should not be<br>
> manipulating them unless there is a serious low-level need to.<br>
><br>
> It looks like you've already addressed the fact that some transports need to<br>
> be platform-specific.  That's not quite accurate, unless you take a very<br>
> broad definition of "platform".  In Twisted, the basic socket-based TCP<br>
> transport is actually supported across all platforms; but some other *APIs*<br>
> (well, let's be honest, right now, just IOCP, but there have been others,<br>
> such as java's native I/O APIs under Jython, in the past).<br>
><br>
> You have to ask the "pollster" (by which I mean: reactor) for transport<br>
> objects, because different multiplexing mechanisms can require different I/O<br>
> APIs, even for basic socket I/O.  This is why I keep talking about IOCP.<br>
> It's not that Windows is particularly great, but that the IOCP API, if used<br>
> correctly, is fairly alien, and is a good proxy for other use-cases which<br>
> are less direct to explain, like interacting with GUI libraries where you<br>
> need to interact with the GUI's notion of a socket to get notifications,<br>
> rather than a raw FD.  (GUI libraries often do this because they have to<br>
> support Windows and therefore IOCP.)  Others in this thread have already<br>
> mentioned the fact that ZeroMQ requires the same sort of affordance.  This<br>
> is really a design error on 0MQ's part, but, you have to deal with it anyway<br>
> ;-).<br>
><br>
> More importantly, concretely tying everything to sockets is just bad design.<br>
> You want to be able to operate on pipes and PTYs (which need to call read(),<br>
> or, a bunch of gross ioctl()s and then read(), not recv()).  You want to be<br>
> able to able to operate on these things in unit tests without involving any<br>
> actual file descriptors or syscalls.  The higher level of abstraction makes<br>
> regular application code a lot shorter, too: I was able to compress<br>
> echosvr.py down to 22 lines by removing all the comments and logging and<br>
> such, but that is still more than twice as long as the (9 line) echo server<br>
> example on the front page of <<a href="http://twistedmatrix.com/trac/" target="_blank">http://twistedmatrix.com/trac/</a>>.  It's closer<br>
> in length to the (19 line) full line-based publish/subscribe protocol over<br>
> on the third tab.<br>
><br>
> Also, what about testing? You want to be able to simulate the order of<br>
> responses of multiple syscalls to coerce your event-driven program to<br>
> receive its events in different orders.  One of the big advantages of event<br>
> driven programming is that everything's just a method call, so your unit<br>
> tests can just call the methods to deliver data to your program and see what<br>
> it does, without needing to have a large, elaborate simulation edifice to<br>
> pretend to be a socket.  But, once you mix in the magic of the generator<br>
> trampoline, it's somewhat hard to assemble your own working environment<br>
> without some kind of test event source; at least, it's not clear to me how<br>
> to assemble a Task without having a pollster anywhere, or how to make my own<br>
> basic pollster for testing.<br>
><br>
> The event loop has two basic ways to register callbacks:<br>
> call_soon(callback, *args) causes callback(*args) to be called the<br>
> next time the event loop runs; call_later(delay, callback, *args)<br>
> schedules a callback at some time (relative or absolute) in the<br>
> future.<br>
><br>
><br>
> "relative or absolute" is hiding the whole monotonic-clocks discussion<br>
> behind a simple phrase, but that probably does not need to be resolved<br>
> here... I'll let you know if we ever figure it out :).<br>
><br>
> sockets.py: <a href="http://code.google.com/p/tulip/source/browse/sockets.py" target="_blank">http://code.google.com/p/tulip/source/browse/sockets.py</a><br>
><br>
> This implements some internet primitives using the APIs in<br>
> scheduling.py (including block_r() and block_w()). I call them<br>
> transports but they are different from transports Twisted; they are<br>
> closer to idealized sockets. SocketTransport wraps a plain socket,<br>
> offering recv() and send() methods that must be invoked using yield<br>
> from.<br>
><br>
><br>
> I feel I should note that these methods behave inconsistently; send()<br>
> behaves as sendall(), re-trying its writes until it receives a full buffer,<br>
> but recv() may yield a short read.<br>
><br>
> (But most importantly, block_r and block_w are insufficient as primitives;<br>
> you need a separate pollster that uses write_then_block(data) and<br>
> read_then_block() too, which may need to dispatch to WSASend/WSARecv or<br>
> WriteFile/ReadFile.)<br>
><br>
> SslTransport wraps an ssl socket (luckily in Python 2.6 and up,<br>
> stdlib ssl sockets have good async support!).<br>
><br>
><br>
> stdlib ssl sockets have async support that makes a number of UNIX-y<br>
> assumptions.  The wrap_socket trick doesn't work with IOCP, because the I/O<br>
> operations are initiated within the SSL layer, and therefore can't be<br>
> associated with a completion port, so they won't cause a queued completion<br>
> status trigger and therefore won't wake up the loop.  This plagued us for<br>
> many years within Twisted and has only relatively recently been fixed:<br>
> <<a href="http://tm.tl/593" target="_blank">http://tm.tl/593</a>>.<br>
><br>
> Since probably 99% of the people on this list don't actually give a crap<br>
> about Windows, let me give a more practical example: you can't do SSL over a<br>
> UNIX pipe.  Off the top of my head, this means you can't write a<br>
> command-line tool to encrypt a connection via a shell pipeline, but there<br>
> are many other cases where you'd expect to be able to get arbitrary I/O over<br>
> stdout.<br>
><br>
> It's reasonable, of course, for lots of Python applications to not care<br>
> about high-performance, high-concurrency SSL on Windows,; select() works<br>
> okay for many applications on Windows.  And most SSL happens on sockets, not<br>
> pipes, hence the existence of the OpenSSL API that the stdlib ssl module<br>
> exposes for wrapping sockets.  But, as I'll explain in a moment, this is one<br>
> reason that it's important to be able to give your code a turbo boost with<br>
> Twisted (or other third-party extensions) once you start encountering<br>
> problems like this.<br>
><br>
> I don't particularly care about the exact abstractions in this module;<br>
> they are convenient and I was surprised how easy it was to add SSL,<br>
> but still these mostly serve as somewhat realistic examples of how to<br>
> use scheduling.py.<br>
><br>
><br>
> This is where I think we really differ.<br>
><br>
> I think that the whole attempt to build a coroutine scheduler at the low<br>
> level is somewhat misguided and will encourage people to write misleading,<br>
> sloppy, incorrect programs that will be tricky to debug (although, to be<br>
> fair, not quite as tricky as even more misleading/sloppy/incorrect<br>
> multi-threaded ones).  However, I'm more than happy to agree to disagree on<br>
> this point: clearly you think that forests of yielding coroutines are a big<br>
> part of the future of Python.  Maybe you're even right to do so, since I<br>
> have no interest in adding language features, whereas if you hit a rough<br>
> edge in 'yield' syntax you can sand it off rather than living with it.  I<br>
> will readily concede that 'yield from' and 'return' are nicer than the<br>
> somewhat ad-hoc idioms we ended up having to contend with in the current<br>
> iteration of @inlineCallbacks.  (Except for the exit-at-a-distance problem,<br>
> which it doesn't seem that return->StopIteration addresses - does this<br>
> happen, with PEP-380 generators?<br>
> <<a href="http://twistedmatrix.com/trac/ticket/4157" target="_blank">http://twistedmatrix.com/trac/ticket/4157</a>>)<br>
><br>
> What I'm not happy to disagree about is the importance of a good I/O<br>
> abstraction and interoperation layer.<br>
><br>
> Twisted is not going away; there are oodles of good reasons that it's built<br>
> the way it is, as I've tried to describe in this and other messages, and<br>
> none of our plans for its future involve putting coroutine trampolines at<br>
> the core of the event loop; those are just fine over on the side with<br>
> inlineCallbacks.  However, lots of Python programmers are going to use what<br>
> you come up with.  They'd use it even if it didn't really work, just because<br>
> it's bundled in and it's convenient.  But I think it'll probably work fine<br>
> for many tasks, and it will appeal to lots of people new to event-driven I/O<br>
> because of the seductive deception of synchronous control flow and the<br>
> superiority to scheduling I/O operations with threads.<br>
><br>
> What I think is really very important in the design of this new system is to<br>
> present an API whereby:<br>
><br>
> if someone wants to write a basic protocol or data-format parser for the<br>
> stdlib, it should be easy to write it as a feed parser without needing<br>
> generator coroutines (for example, if they're pushing data into a C library,<br>
> they shouldn't have to write a while loop that calls recv, they should be<br>
> able to just transform some data callback into Python into some data<br>
> callback in C; it should be able to leverage tulip without much more work,<br>
> if users of tulip (read; the stdlib) need access to some functionality<br>
> implemented within Twisted, like an event-driven DNS client that is more<br>
> scalable than getaddrinfo, they can call into Twisted without re-writing<br>
> their entire program,<br>
> if users of Twisted need to invoke some functionality implemented on top of<br>
> tulip, they can construct a task and weave in a scheduler, similarly without<br>
> re-writing much,<br>
> if users of tulip want to just use Twisted to get better performance or<br>
> reliability than the built-in stdlib multiplexor, they ideally shouldn't<br>
> have to change anything, just run it with a different import line or<br>
> something, and<br>
> if (when) users of tulip realize that their generators have devolved into a<br>
> mess of spaghetti ;-) and they need to migrate to Twisted-style event-driven<br>
> callbacks and maybe some formal state machines or generated parsers to deal<br>
> with their inputs, that process can be done incrementally and not in one<br>
> giant shoot-the-moon effort which will make them hate Twisted.<br>
><br>
><br>
> As an added bonus, such an API would provide a great basis for Tornado and<br>
> Twisted to interoperate.<br>
><br>
> It would also be nice to have a more discrete I/O layer to insulate<br>
> application code from common foibles like the fact that, for example, if you<br>
> call send() in tulip multiple times but forget to 'yield from ...send()',<br>
> you may end up writing interleaved garbage on the connection, then raising<br>
> an assertion error, but only if there's a sufficient quantity of data and it<br>
> needs to block; it will otherwise appear to work, leading to bugs that only<br>
> start happening when you are pushing large volumes of data through a system<br>
> at rates exceeding wire speed.  In other words, "only in production, only<br>
> during the holiday season, only during traffic spikes, only when it's really<br>
> really important for the system to keep working".<br>
><br>
> This is why I think that step 1 here needs to be a common low-level API for<br>
> event-triggered operations that does not have anything to do with<br>
> generators.  I don't want to stop you from doing interesting things with<br>
> generators, but I do really want to decouple the tasks so that their<br>
> responsibilities are not unnecessarily conflated.<br>
><br>
> task.unblock() is a method; protocol.data_received is a method.  Both can be<br>
> invoked at the same level by an event loop.  Once that low-level event loop<br>
> is delivering data to that callback's satisfaction, the callbacks can<br>
> happily drive a coroutine scheduler, and the coroutine scheduler can have<br>
> much less of a deep integration with the I/O itself; it just needs some kind<br>
> of sentinel object (a Future, a Deferred) to keep track of what exactly it's<br>
> waiting for.<br>
><br>
> I'm most interested in feedback on the design of polling.py and<br>
> scheduling.py, and to a lesser extent on the design of sockets.py;<br>
> main.py is just an example of how this style works out in practice.<br>
><br>
><br>
> It looks to me like there's a design error in scheduling.py with respect to<br>
> coordinating concurrent operations.  If you try to block on two operations<br>
> at once, you'll get an assertion error ('assert not self.blocked', in<br>
> block), so you can't coordinate two interesting I/O requests without<br>
> spawning a bunch of new Tasks and then having them unblock their parent Task<br>
> when they're done.  I may just be failing to imagine how one would implement<br>
> something like Twisted's gatherResults, but this looks like it would be<br>
> frustrating, tedious, and involve creating lots of extra objects and making<br>
> the scheduler do a bunch more work.<br>
><br>
> Also, shouldn't there be a lot more real exceptions and a lot fewer<br>
> assertions in this code?<br>
><br>
> Relatedly, add_reader/writer will silently stomp on a previous FD<br>
> registration, so if two tasks end up calling recv() on the same socket, it<br>
> doesn't look like there's any way to find out that they both did that.  It<br>
> looks like the first task to call it will just hang forever, and the second<br>
> one will "win"?  What are the intended semantics?<br>
><br>
> Speaking from the perspective of I/O scheduling, it will also be thrashing<br>
> any stateful multiplexor with a ton of unnecessary syscalls.  A Twisted<br>
> protocol in normal operation just receiving data from a single connection,<br>
> using, let's say, a kqueue-based multiplexor will call kevent() once to<br>
> register interest, then kqueue() to block, and then just keep getting<br>
> data-available notifications and processing them unless some downstream<br>
> buffer fills up and the transport is told to pause producing data, at which<br>
> point another kevent() gets issued.  tulip, by contrast, will call kevent()<br>
> over and over again, removing and then re-adding its reader repeatedly for<br>
> every packet, since it can never know if someone is about to call recv()<br>
> again any time soon.  Once again, request/response is not the best model for<br>
> retrieving data from a transport; active connections need to be prepared to<br>
> receive more data at any time and not in response to any particular request.<br>
><br>
> Finally, apologies for spelling / grammar errors; I didn't have a lot of<br>
> time to copy-edit.<br>
><br>
> -glyph<br>
><br>
</div></div><div class="HOEnZb"><div class="h5">> _______________________________________________<br>
> Python-ideas mailing list<br>
> <a href="mailto:Python-ideas@python.org">Python-ideas@python.org</a><br>
> <a href="http://mail.python.org/mailman/listinfo/python-ideas" target="_blank">http://mail.python.org/mailman/listinfo/python-ideas</a><br>
><br>
</div></div></blockquote></div><br><br clear="all"><br>-- <br>--Guido van Rossum (<a href="http://python.org/~guido">python.org/~guido</a>)<br>
</div>