Reputation: 3436
I wanted to write a server that a client could connect to and receive periodic updates without having to poll. The problem I have experienced with asyncore is that if you do not return true when dispatcher.writable() is called, you have to wait until after the asyncore.loop has timed out (default is 30s).
The two ways I have tried to work around this is 1) reduce timeout to a low value or 2) query connections for when they will next update and generate an adequate timeout value. However if you refer to 'Select Law' in 'man 2 select_tut', it states, "You should always try to use select() without a timeout."
Is there a better way to do this? Twisted maybe? I wanted to try and avoid extra threads. I'll include the variable timeout example here:
#!/usr/bin/python
import time
import socket
import asyncore
# in seconds
UPDATE_PERIOD = 4.0
class Channel(asyncore.dispatcher):
def __init__(self, sock, sck_map):
asyncore.dispatcher.__init__(self, sock=sock, map=sck_map)
self.last_update = 0.0 # should update immediately
self.send_buf = ''
self.recv_buf = ''
def writable(self):
return len(self.send_buf) > 0
def handle_write(self):
nbytes = self.send(self.send_buf)
self.send_buf = self.send_buf[nbytes:]
def handle_read(self):
print 'read'
print 'recv:', self.recv(4096)
def handle_close(self):
print 'close'
self.close()
# added for variable timeout
def update(self):
if time.time() >= self.next_update():
self.send_buf += 'hello %f\n'%(time.time())
self.last_update = time.time()
def next_update(self):
return self.last_update + UPDATE_PERIOD
class Server(asyncore.dispatcher):
def __init__(self, port, sck_map):
asyncore.dispatcher.__init__(self, map=sck_map)
self.port = port
self.sck_map = sck_map
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.bind( ("", port))
self.listen(16)
print "listening on port", self.port
def handle_accept(self):
(conn, addr) = self.accept()
Channel(sock=conn, sck_map=self.sck_map)
# added for variable timeout
def update(self):
pass
def next_update(self):
return None
sck_map = {}
server = Server(9090, sck_map)
while True:
next_update = time.time() + 30.0
for c in sck_map.values():
c.update() # <-- fill write buffers
n = c.next_update()
#print 'n:',n
if n is not None:
next_update = min(next_update, n)
_timeout = max(0.1, next_update - time.time())
asyncore.loop(timeout=_timeout, count=1, map=sck_map)
Upvotes: 8
Views: 8548
Reputation: 1650
Maybe I don't understand what the OP was trying to accomplish, but I just solved this issue using 1 thread that gets a weakref of each Channel(asyncore.dispatcher) object. This thread determines its own timing and will send the Channel an update periodically using a Queue in that channel. It gets the Queue from the Channel object by calling getQueue.
The reason I use a weakref is because clients are transient. If the channel dies then the weakref returns None. That way the timing thread doesn't keep old objects alive because it references them.
I know the OP wanted to avoid threads, but this solution is very simple. It only ever creates one thread and it talks to any Channels that are created as the Server object adds them to the threads list of objects to monitor.
Upvotes: 0
Reputation: 6778
This is basically demiurgus' solution with the rough edges made round. It retains his basic idea, but prevents RuntimeErrors and busy loops and is tested. [Edit: resolved issues with modifying the scheduler during _delay]
class asynschedcore(sched.scheduler):
"""Combine sched.scheduler and asyncore.loop."""
# On receiving a signal asyncore kindly restarts select. However the signal
# handler might change the scheduler instance. This tunable determines the
# maximum time in seconds to spend in asycore.loop before reexamining the
# scheduler.
maxloop = 30
def __init__(self, map=None):
sched.scheduler.__init__(self, time.time, self._delay)
if map is None:
self._asynmap = asyncore.socket_map
else:
self._asynmap = map
self._abort_delay = False
def _maybe_abort_delay(self):
if not self._abort_delay:
return False
# Returning from this function causes the next event to be executed, so
# it might be executed too early. This can be avoided by modifying the
# head of the queue. Also note that enterabs sets _abort_delay to True.
self.enterabs(0, 0, lambda:None, ())
self._abort_delay = False
return True
def _delay(self, timeout):
if self._maybe_abort_delay():
return
if 0 == timeout:
# Should we support this hack, too?
# asyncore.loop(0, map=self._asynmap, count=1)
return
now = time.time()
finish = now + timeout
while now < finish and self._asynmap:
asyncore.loop(min(finish - now, self.maxloop), map=self._asynmap,
count=1)
if self._maybe_abort_delay():
return
now = time.time()
if now < finish:
time.sleep(finish - now)
def enterabs(self, abstime, priority, action, argument):
# We might insert an event before the currently next event.
self._abort_delay = True
return sched.scheduler.enterabs(self, abstime, priority, action,
argument)
# Overwriting enter is not necessary, because it is implemented using enter.
def cancel(self, event):
# We might cancel the next event.
self._abort_delay = True
return sched.scheduler.cancel(self, event)
def run(self):
"""Runs as long as either an event is scheduled or there are
sockets in the map."""
while True:
if not self.empty():
sched.scheduler.run(self)
elif self._asynmap:
asyncore.loop(self.maxloop, map=self._asynmap, count=1)
else:
break
Upvotes: 2
Reputation: 41
Maybe you can do this with sched.scheduler
, like this (n.b. not tested):
import sched, asyncore, time
# Create a scheduler with a delay function that calls asyncore.loop
scheduler = sched.scheduler(time.time, lambda t: _poll_loop(t, time.time()) )
# Add the update timeouts with scheduler.enter
# ...
def _poll_loop(timeout, start_time):
asyncore.loop(timeout, count=1)
finish_time = time.time()
timeleft = finish_time - start_time
if timeleft > timeout: # there was a message and the timeout delay is not finished
_poll_loop(timeleft, finish_time) # so wait some more polling the socket
def main_loop():
while True:
if scheduler.empty():
asyncore.loop(30.0, count=1) # just default timeout, use what suits you
# add other work that might create scheduled events here
else:
scheduler.run()
Upvotes: 4
Reputation: 27285
I would use Twisted, long time since I used asyncore but I think this should be the twisted equivalent (not tested, written from memory):
from twisted.internet import reactor, protocol
import time
UPDATE_PERIOD = 4.0
class MyClient(protocol.Protocol):
def connectionMade(self):
self.updateCall = reactor.callLater(UPDATE_PERIOD, self.update)
def connectionLost(self, reason):
self.updateCall.cancel()
def update(self):
self.transport.write("hello %f\n" % (time.time(),))
def dataReceived(self, data):
print "recv:", data
f = protocol.ServerFactory()
f.protocol = MyClient
reactor.listenTCP(9090, f)
reactor.run()
Upvotes: 1
Reputation: 127447
The "select law" doesn't apply to your case, as you have not only client-triggered (pure server) activities, but also time-triggered activities - this is precisely what the select timeout is for. What the law should really say is "if you specify a timeout, make sure you actually have to do something useful when the timeout arrives". The law is meant to protect against busy-waiting; your code does not busy-wait.
I would not set _timeout to the maximum of 0.1 and the next update time, but to the maximum of 0.0 and the next timeout. IOW, if an update period has expired while you were doing updates, you should do that specific update right away.
Instead of asking each channel every time whether it wants to update, you could store all channels in a priority queue (sorted by next-update time), and then only run update for the earliest channels (until you find one whose update time has not arrived). You can use the heapq module for that.
You can also save a few system calls by not having each channel ask for the current time, but only poll the current time once, and pass it to .update.
Upvotes: 4