[Twisted-Python] Learning about IPushProducer
When running the following code (my 2nd twisted program!), it works as I had hoped - it doesn't starve any clients that want to receive data back, even with a simultaneously active really long streaming server-to-client communication (i.e. one piggy client asking for millions of bytes). i.e. another client can get in and ask for just a few bytes while a large payload is being delivered to a different client. Which is great! Here's a sample interaction from the client side: $ telnet localhost 8007 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 1 x 2 xx 3 xxx 10 xxxxxxxxxx 99999 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [...lots of x's...] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx bye Connection closed by foreign host. $ So I have 2 questions on my code: 1) am I doing anything wrong in setting up the plumbing? 2) does pauseProducing() get called by another thread whilst resumeProducing() is running? (I believe it must, otherwise my resumeProducing() would only be entered once). If so I should have an appropriate mutex around the read/write of self.pause, no? Here is the code, and output from the server is at the end. Thanks -- Benjamin #!/usr/bin/env python import os, os.path, sys, re, commands, pickle, tempfile, getopt, datetime import socket, string, random, time, traceback, shutil, popen2 from zope.interface import implements from twisted.internet import protocol, defer, interfaces, error, reactor from twisted.internet.protocol import Protocol, Factory from twisted.protocols.basic import LineReceiver class NonStarvingXGiver: implements(interfaces.IPushProducer) def __init__(self, howmany, consumer): self.howmany = howmany self.sent_already = 0 self.paused = False self.consumer = consumer def beginSendingXs(self): self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, False) return deferred def pauseProducing(self): print 'pauseProducing: invoked' self.paused = True def resumeProducing(self): print 'resumeProducing: invoked' self.paused = False maxchunksz = 1024 while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) self.consumer.write('x' * chunksz) self.sent_already += chunksz if self.howmany == self.sent_already: self.consumer.write('\n') self.consumer.unregisterProducer() print 'resumeProducing: exiting for the last time' def stopProducing(self): print 'stopProducing: invoked' self.consumer.unregisterProducer() class xgiver(LineReceiver): def lineReceived(self, howmany): print 'got line [%s] from client [%s]' % (howmany, self.transport.getPeer()) if howmany == 'bye': print 'goodbye to', self.transport.getPeer() self.transport.loseConnection() return try: howmany = int(howmany) s = NonStarvingXGiver(howmany, self.transport) s.beginSendingXs() except Exception, ex: self.transport.write("invalid input " + howmany + "\n") # Next lines are magic: factory = Factory() factory.protocol = xgiver # 8007 is the port you want to run under. Choose something >1024 reactor.listenTCP(8007, factory) reactor.run() ------------------------------------------------------------------- Server output: $ ./xgiver.py got line [1] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [2] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [3] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [10] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [99999] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked pauseProducing: invoked resumeProducing: invoked resumeProducing: exiting for the last time got line [bye] from client [IPv4Address(TCP, '127.0.0.1', 51007)] goodbye to IPv4Address(TCP, '127.0.0.1', 51007)
Hi. Anyone have any pointers as to how I can get some of my questions answered below? I had hoped to get some response. Did I not use the proper etiquitte? Or there is some expert on the IPushProducer mechanism or the author of page http://twistedmatrix.com/projects/core/documentation/howto/producers.htm l that I can be referred to that perhaps isn't reading this list? Thanks, Benjamin Rutt ________________________________ From: twisted-python-bounces@twistedmatrix.com [mailto:twisted-python-bounces@twistedmatrix.com] On Behalf Of Rutt, Benjamin Sent: Tuesday, March 06, 2007 12:04 PM To: twisted-python@twistedmatrix.com Subject: [Twisted-Python] Learning about IPushProducer When running the following code (my 2nd twisted program!), it works as I had hoped - it doesn't starve any clients that want to receive data back, even with a simultaneously active really long streaming server-to-client communication (i.e. one piggy client asking for millions of bytes). i.e. another client can get in and ask for just a few bytes while a large payload is being delivered to a different client. Which is great! Here's a sample interaction from the client side: $ telnet localhost 8007 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 1 x 2 xx 3 xxx 10 xxxxxxxxxx 99999 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [...lots of x's...] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx bye Connection closed by foreign host. $ So I have 2 questions on my code: 1) am I doing anything wrong in setting up the plumbing? 2) does pauseProducing() get called by another thread whilst resumeProducing() is running? (I believe it must, otherwise my resumeProducing() would only be entered once). If so I should have an appropriate mutex around the read/write of self.pause, no? Here is the code, and output from the server is at the end. Thanks -- Benjamin #!/usr/bin/env python import os, os.path, sys, re, commands, pickle, tempfile, getopt, datetime import socket, string, random, time, traceback, shutil, popen2 from zope.interface import implements from twisted.internet import protocol, defer, interfaces, error, reactor from twisted.internet.protocol import Protocol, Factory from twisted.protocols.basic import LineReceiver class NonStarvingXGiver: implements(interfaces.IPushProducer) def __init__(self, howmany, consumer): self.howmany = howmany self.sent_already = 0 self.paused = False self.consumer = consumer def beginSendingXs(self): self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, False) return deferred def pauseProducing(self): print 'pauseProducing: invoked' self.paused = True def resumeProducing(self): print 'resumeProducing: invoked' self.paused = False maxchunksz = 1024 while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) self.consumer.write('x' * chunksz) self.sent_already += chunksz if self.howmany == self.sent_already: self.consumer.write('\n') self.consumer.unregisterProducer() print 'resumeProducing: exiting for the last time' def stopProducing(self): print 'stopProducing: invoked' self.consumer.unregisterProducer() class xgiver(LineReceiver): def lineReceived(self, howmany): print 'got line [%s] from client [%s]' % (howmany, self.transport.getPeer()) if howmany == 'bye': print 'goodbye to', self.transport.getPeer() self.transport.loseConnection() return try: howmany = int(howmany) s = NonStarvingXGiver(howmany, self.transport) s.beginSendingXs() except Exception, ex: self.transport.write("invalid input " + howmany + "\n") # Next lines are magic: factory = Factory() factory.protocol = xgiver # 8007 is the port you want to run under. Choose something >1024 reactor.listenTCP(8007, factory) reactor.run() ------------------------------------------------------------------- Server output: $ ./xgiver.py got line [1] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [2] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [3] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [10] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [99999] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked pauseProducing: invoked resumeProducing: invoked resumeProducing: exiting for the last time got line [bye] from client [IPv4Address(TCP, '127.0.0.1', 51007)] goodbye to IPv4Address(TCP, '127.0.0.1', 51007)
On Mon, 12 Mar 2007 15:44:41 -0400, "Rutt, Benjamin" <benjamin.rutt@gs.com> wrote:
Hi.
Anyone have any pointers as to how I can get some of my questions answered below? I had hoped to get some response. Did I not use the proper etiquitte? Or there is some expert on the IPushProducer mechanism or the author of page
Sorry, your question was big and challenging to approach.
[snip]
When running the following code (my 2nd twisted program!), it works as I had hoped - it doesn't starve any clients that want to receive data back, even with a simultaneously active really long streaming server-to-client communication (i.e. one piggy client asking for millions of bytes). i.e. another client can get in and ask for just a few bytes while a large payload is being delivered to a different client. Which is great!
Here's a sample interaction from the client side:
$ telnet localhost 8007 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 1 x 2 xx 3 xxx 10 xxxxxxxxxx 99999
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[...lots of x's...] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx bye Connection closed by foreign host. $
So I have 2 questions on my code:
1) am I doing anything wrong in setting up the plumbing? 2) does pauseProducing() get called by another thread whilst resumeProducing() is running? (I believe it must, otherwise my resumeProducing() would only be entered once). If so I should have an appropriate mutex around the read/write of self.pause, no?
Here is the code, and output from the server is at the end. Thanks -- Benjamin
#!/usr/bin/env python import os, os.path, sys, re, commands, pickle, tempfile, getopt, datetime import socket, string, random, time, traceback, shutil, popen2
from zope.interface import implements from twisted.internet import protocol, defer, interfaces, error, reactor from twisted.internet.protocol import Protocol, Factory from twisted.protocols.basic import LineReceiver
class NonStarvingXGiver: implements(interfaces.IPushProducer) def __init__(self, howmany, consumer): self.howmany = howmany self.sent_already = 0 self.paused = False self.consumer = consumer def beginSendingXs(self): self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, False) return deferred def pauseProducing(self): print 'pauseProducing: invoked' self.paused = True def resumeProducing(self): print 'resumeProducing: invoked' self.paused = False maxchunksz = 1024
This loop:
while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) self.consumer.write('x' * chunksz) self.sent_already += chunksz
is a bit atypical, I think. The reason it is eventually stopping is that your code is being invoked re-entrantly by the consumer as soon as it decides its buffer is full. I'm not sure the loop is /wrong/, but it is a bit surprising. You don't need a mutex here, since it's single threaded, but you do need to be aware that your code can be re-entered within a single thread. Does that answer your questions?
[snip]
Jean-Paul
Sorry, your question was big and challenging to approach. Understood, thanks. I should have pared it down. This loop: > while not self.paused and self.howmany > >self.sent_already: > chunksz = min(maxchunksz, self.howmany - >self.sent_already) > self.consumer.write('x' * chunksz) > self.sent_already += chunksz is a bit atypical, I think. The reason it is eventually stopping is that your code is being invoked re-entrantly by the consumer as soon as it decides its buffer is full. I'm not sure the loop is /wrong/, but it is a bit surprising. You don't need a mutex here, since it's single threaded, but you do need to be aware that your code can be re-entered within a single thread. Does that answer your questions? Yes, thank you, it answers some of them but it raises more :). I see, it's self.consumer.write(...) that ends up calling the pause method. So that's how it's reentrant. I knew twisted wasn't multithreaded in this case. Makes sense. I have since made my resumeProducing() code loop for up to 100 (or 500, or 1000 etc.) iterations and then return. Thus I'm treating resumeProducing as if it should "produce a chunk larger than 1 byte but smaller that the whole dataset" then return. How is this approach, does it better match the intention of the producer/consumer system vs. the "loop forever until paused" scheme I had earlier? A final question -- is it safe for my code to ever call pauseProducing, stopProducing, resumeProducing directly? Or that will mess up the balance of the twisted universe? (I'm thinking that the calls to these 3 methods should only originate "from within twisted" in case the whole producer/consumer system is tracking how many times it calls each one etc.) I don't plan on calling them directly, but want to critically review in an informed way my colleague's code which does this. Thank you so much for the response! Benjamin Rutt
On Mon, 12 Mar 2007 17:46:54 -0400, "Rutt, Benjamin" <benjamin.rutt@gs.com> wrote:
Sorry, your question was big and challenging to approach.
Understood, thanks. I should have pared it down.
This loop:
while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) self.consumer.write('x' * chunksz) self.sent_already += chunksz
is a bit atypical, I think. The reason it is eventually stopping is that your code is being invoked re-entrantly by the consumer as soon as it decides its buffer is full. I'm not sure the loop is /wrong/, but it is a bit surprising. You don't need a mutex here, since it's single threaded, but you do need to be aware that your code can be re-entered within a single thread.
Does that answer your questions?
Yes, thank you, it answers some of them but it raises more :). I see, it's self.consumer.write(...) that ends up calling the pause method. So that's how it's reentrant. I knew twisted wasn't multithreaded in this case. Makes sense.
I have since made my resumeProducing() code loop for up to 100 (or 500, or 1000 etc.) iterations and then return. Thus I'm treating resumeProducing as if it should "produce a chunk larger than 1 byte but smaller that the whole dataset" then return. How is this approach, does it better match the intention of the producer/consumer system vs. the "loop forever until paused" scheme I had earlier?
That's more like what I expect to see in a resumeProducing, yea. Of course, paused might still get set before your loop decides to exit, if you are no longer checking paused. This isn't disasterous, but it means some information about what a good amount of stuff to keep in memory is. I think the most common thing for a resumeProducing implementation to do is build up a chunk of data and then call write on the consumer just once. So it doesn't end up mattering if you get paused or not, since you're done any way by the time that happens. I think you have the basic idea of how producers should work, though, and now I'm just blabbing. :)
A final question -- is it safe for my code to ever call pauseProducing, stopProducing, resumeProducing directly? Or that will mess up the balance of the twisted universe? (I'm thinking that the calls to these 3 methods should only originate "from within twisted" in case the whole producer/consumer system is tracking how many times it calls each one etc.) I don't plan on calling them directly, but want to critically review in an informed way my colleague's code which does this.
The consumer you register the producer with basically gets control of those methods for as long as the producer remains registered. They're there for it to communicate its needs with the producer, so having any other code call them is probably bad in general, although it might work in specific cases.
Thank you so much for the response!
No problem. :) Jean-Paul
BTW, how was it that my pauseProducing ever got invoked below? after all I registered my producer (erroneously, no doubt since it implements IPushProducer) as a pull producer by using the 2nd argument of 'False' below: self.consumer.registerProducer(self, False) If indeed it's registered as a pull producer I wouldn't think its pause should ever be called. Does twisted actually use the type of the class to see what to call? Or getattr(class,'pauseProducing') or somesuch? If so, what's the purpose of 'True' or 'False' during registration? from the docs: registerProducer(producer, streaming) So that a consumer can invoke methods on a producer, the consumer needs to be told about the producer. This is done with the registerProducer method. The first argument is either a IPullProducer or IPushProducer provider; the second argument indicates which of these interfaces is provided: True for push producers, False for pull producers. Thanks! ________________________________ From: twisted-python-bounces@twistedmatrix.com [mailto:twisted-python-bounces@twistedmatrix.com] On Behalf Of Rutt, Benjamin Sent: Tuesday, March 06, 2007 12:04 PM To: twisted-python@twistedmatrix.com Subject: [Twisted-Python] Learning about IPushProducer When running the following code (my 2nd twisted program!), it works as I had hoped - it doesn't starve any clients that want to receive data back, even with a simultaneously active really long streaming server-to-client communication (i.e. one piggy client asking for millions of bytes). i.e. another client can get in and ask for just a few bytes while a large payload is being delivered to a different client. Which is great! Here's a sample interaction from the client side: $ telnet localhost 8007 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 1 x 2 xx 3 xxx 10 xxxxxxxxxx 99999 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [...lots of x's...] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx bye Connection closed by foreign host. $ So I have 2 questions on my code: 1) am I doing anything wrong in setting up the plumbing? 2) does pauseProducing() get called by another thread whilst resumeProducing() is running? (I believe it must, otherwise my resumeProducing() would only be entered once). If so I should have an appropriate mutex around the read/write of self.pause, no? Here is the code, and output from the server is at the end. Thanks -- Benjamin #!/usr/bin/env python import os, os.path, sys, re, commands, pickle, tempfile, getopt, datetime import socket, string, random, time, traceback, shutil, popen2 from zope.interface import implements from twisted.internet import protocol, defer, interfaces, error, reactor from twisted.internet.protocol import Protocol, Factory from twisted.protocols.basic import LineReceiver class NonStarvingXGiver: implements(interfaces.IPushProducer) def __init__(self, howmany, consumer): self.howmany = howmany self.sent_already = 0 self.paused = False self.consumer = consumer def beginSendingXs(self): self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, False) return deferred def pauseProducing(self): print 'pauseProducing: invoked' self.paused = True def resumeProducing(self): print 'resumeProducing: invoked' self.paused = False maxchunksz = 1024 while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) self.consumer.write('x' * chunksz) self.sent_already += chunksz if self.howmany == self.sent_already: self.consumer.write('\n') self.consumer.unregisterProducer() print 'resumeProducing: exiting for the last time' def stopProducing(self): print 'stopProducing: invoked' self.consumer.unregisterProducer() class xgiver(LineReceiver): def lineReceived(self, howmany): print 'got line [%s] from client [%s]' % (howmany, self.transport.getPeer()) if howmany == 'bye': print 'goodbye to', self.transport.getPeer() self.transport.loseConnection() return try: howmany = int(howmany) s = NonStarvingXGiver(howmany, self.transport) s.beginSendingXs() except Exception, ex: self.transport.write("invalid input " + howmany + "\n") # Next lines are magic: factory = Factory() factory.protocol = xgiver # 8007 is the port you want to run under. Choose something >1024 reactor.listenTCP(8007, factory) reactor.run() ------------------------------------------------------------------- Server output: $ ./xgiver.py got line [1] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [2] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [3] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [10] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked resumeProducing: exiting for the last time got line [99999] from client [IPv4Address(TCP, '127.0.0.1', 51007)] resumeProducing: invoked pauseProducing: invoked resumeProducing: invoked resumeProducing: exiting for the last time got line [bye] from client [IPv4Address(TCP, '127.0.0.1', 51007)] goodbye to IPv4Address(TCP, '127.0.0.1', 51007)
On Fri, 16 Mar 2007 11:48:53 -0400, "Rutt, Benjamin" <benjamin.rutt@gs.com> wrote:
BTW, how was it that my pauseProducing ever got invoked below? after all I registered my producer (erroneously, no doubt since it implements IPushProducer) as a pull producer by using the 2nd argument of 'False' below:
self.consumer.registerProducer(self, False)
If indeed it's registered as a pull producer I wouldn't think its pause should ever be called. Does twisted actually use the type of the class to see what to call? Or getattr(class,'pauseProducing') or somesuch? If so, what's the purpose of 'True' or 'False' during registration?
from the docs:
registerProducer(producer, streaming)
So that a consumer can invoke methods on a producer, the consumer needs to be told about the producer. This is done with the registerProducer method. The first argument is either a IPullProducer or IPushProducer provider; the second argument indicates which of these interfaces is provided: True for push producers, False for pull producers.
This was a bug in one of the consumer implementations in Twisted (#2286). It's been fixed since Twisted 2.5, so this won't happen in Twisted 2.6. Jean-Paul
Thanks. One more question. Any ideas why my server doesn't return any data at all with the 2nd argument to registration being True? It just seems to sit there, but if I flip it to False it works as desired. (This must be why I set it to False earlier, to get it to run). I'm using python 2.4 and twisted 2.0.0. Trivially modified server code, server output and client output are below, separated by ======================. Thanks. #!/usr/bin/env python import os, os.path, sys, re, commands, pickle, tempfile, getopt, datetime import socket, string, random, time, traceback, shutil, popen2 from zope.interface import implements from twisted.internet import protocol, defer, interfaces, error, reactor from twisted.internet.protocol import Protocol, Factory from twisted.protocols.basic import LineReceiver class NonStarvingXGiver: implements(interfaces.IPushProducer) def __init__(self, howmany, consumer): self.howmany = howmany self.sent_already = 0 self.paused = False self.consumer = consumer self.act_as_pull = 0 self.payload1024 = 'x'*1024 def beginSendingXs(self): self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, True) return deferred def pauseProducing(self): print 'pauseProducing: invoked at %d bytes' % (self.sent_already) self.paused = True def resumeProducing(self): if not self.act_as_pull: print 'resumeProducing: invoked' self.paused = False maxchunksz = 1024 while not self.paused and self.howmany > self.sent_already: chunksz = min(maxchunksz, self.howmany - self.sent_already) if chunksz == 1024: self.consumer.write(self.payload1024) else: self.consumer.write('x' * chunksz) self.sent_already += chunksz if self.act_as_pull: break if self.howmany == self.sent_already: self.consumer.write('\n') self.consumer.unregisterProducer() print 'resumeProducing: exiting for the last time' def stopProducing(self): print 'stopProducing: invoked' self.consumer.unregisterProducer() class xgiver(LineReceiver): def lineReceived(self, howmany): print 'got line [%s] from client [%s]' % (howmany, self.transport.getPeer()) if howmany == 'bye': print 'goodbye to', self.transport.getPeer() self.transport.loseConnection() return try: howmany = int(howmany) s = NonStarvingXGiver(howmany, self.transport) s.beginSendingXs() except Exception, ex: traceback.print_exc() self.transport.write("invalid input " + `howmany` + "\n") # Next lines are magic: factory = Factory() factory.protocol = xgiver # 8007 is the port you want to run under. Choose something >1024 reactor.listenTCP(8007, factory) reactor.run() ====================== got line [5] from client [IPv4Address(TCP, '127.0.0.1', 34840)] got line [4] from client [IPv4Address(TCP, '127.0.0.1', 34840)] Traceback (most recent call last): File "./xgiver.py", line 77, in lineReceived s.beginSendingXs() File "./xgiver.py", line 39, in beginSendingXs self.consumer.registerProducer(self, True) File "/sw/external/python-2.4/lib/python2.4/site-packages/twisted/internet/ab stract.py", line 266, in registerProducer raise RuntimeError("Cannot register producer %s, because producer %s was never unregistered." % (producer, self.producer)) RuntimeError: Cannot register producer <__main__.NonStarvingXGiver instance at 0x3a5120>, because producer <__main__.NonStarvingXGiver instance at 0x3a5198> was never unregistered. stopProducing: invoked ====================== $ telnet localhost 8007 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 5 4 invalid input 4
participants (2)
-
Jean-Paul Calderone
-
Rutt, Benjamin