Skip to content

Commit

Permalink
Fixes #195. Changed event loops to support a poll only link and chang…
Browse files Browse the repository at this point in the history
…ed the stack class to use that
  • Loading branch information
TD22057 committed Feb 23, 2020
1 parent c6e1120 commit 539b62f
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 80 deletions.
8 changes: 8 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Revision Change History

## [0.7.2]

### Fixes

- Fixed an issue causing 100% cpu usage introduced in the scene sync code.
([Issue #195)[I195])

## [0.7.1]

### Fixes
Expand Down Expand Up @@ -355,3 +362,4 @@
[I189]: https://github.com/TD22057/insteon-mqtt/issues/189
[I192]: https://github.com/TD22057/insteon-mqtt/issues/192
[I193]: https://github.com/TD22057/insteon-mqtt/issues/193
[I195]: https://github.com/TD22057/insteon-mqtt/issues/195
2 changes: 1 addition & 1 deletion insteon_mqtt/cmd_line/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def start(args, cfg):
# Add the clients to the event loop.
loop.add(mqtt_link, connected=False)
loop.add(plm_link, connected=False)
loop.add(stack_link, connected=True)
loop.add_poll(stack_link)

# Create the insteon message protocol, modem, and MQTT handler and
# link them together.
Expand Down
81 changes: 7 additions & 74 deletions insteon_mqtt/network/Stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Stack class definition.
#
#===========================================================================
import tempfile
from ..Signal import Signal
from .. import log

Expand All @@ -14,7 +13,11 @@ class Stack:
"""A Fake Network Interface for Queueing and 'Asynchronously' Running
Functional Calls
This class appears as though it is a read/write interface that is handled
This is a polling only network "link". Unlike regular links that do read
and write operations when they report they are ready, this class is
designed to only be polled during the event loop.
This is like a network link for reading and writing but that is handled
my the network manager. But in reality it is just a wrapper for inserting
function calls into the network loop. This allows long functional calls
to be broken up into multiple sub calls that can be called on seperate
Expand All @@ -35,56 +38,15 @@ def __init__(self):
# Sent when the link is going down. signature: (Link link)
self.signal_closing = Signal()

# Sent when the link changes state on whether or not it has bytes
# that need to be written to the link. signature: (Link link, bool
# write_active)
self.signal_needs_write = Signal()

# The manager will emit this after the connection has been
# established and everything is ready. Links should usually not emit
# this directly. signature: (Link link, bool connected)
self.signal_connected = Signal()

# Generate a fileno using a temp file. We could just pass 0, which
# seems to work, but not sure if this could cause a subtle bug. It
# might be better to have this be an actual file
self.tempfile = tempfile.TemporaryFile()

# The list of groups of functions to call. Each item should be a
# StackGroup
self.groups = []

#-----------------------------------------------------------------------
def retry_connect_dt(self):
"""Return a positive integer (seconds) if the link should reconnect.
If this returns None, the link will not be reconnected if it closes.
Otherwise this is the retry interval in seconds to try and reconnect
the link by calling connect().
"""
return None

#-----------------------------------------------------------------------
def connect(self):
"""Connect the link to the device.
This should connect to the socket, serial port, file, etc.
Returns:
bool: Returns True if the connection was successful or False it
it failed.
"""
return True

#-----------------------------------------------------------------------
def fileno(self):
"""Return the file descriptor to watch for this link.
Returns:
int: Returns the descriptor (obj.fileno() usually) to monitor.
"""
return self.tempfile

#-----------------------------------------------------------------------
def poll(self, t):
"""Periodic poll callback.
Expand Down Expand Up @@ -137,47 +99,18 @@ def new(self, error_stop=True):
self.groups.append(new_stack)
return new_stack

#-----------------------------------------------------------------------
def read_from_link(self):
"""Read data from the link.
This will be called by the manager when there is data available on
the file descriptor for reading.
Returns:
int: Return -1 if the link had an error. Or any other integer
to indicate success.
"""
raise NotImplementedError("%s.read_from_link() not implemented" %
self.__class__)

#-----------------------------------------------------------------------
def write_to_link(self, t):
"""Write data from the link.
This will be called by the manager when the file descriptor can be
written to. It will only be called after the link as emitted the
signal_needs_write(True). Once all the data has been written, the
link should call self.signal_needs_write.emit(False).
Args:
t (float): The current time (time.time).
"""
raise NotImplementedError("%s.write_to_link() not implemented" %
self.__class__)

#-----------------------------------------------------------------------
def close(self):
"""Close the link.
The link must call self.signal_closing.emit() after closing.
"""
raise NotImplementedError("%s.close() not implemented" %
self.__class__)
self.signal_closing.emit()

#-----------------------------------------------------------------------


#===========================================================================
class StackGroup:
"""A Simple Class for Grouping Functional Calls
Expand Down
41 changes: 39 additions & 2 deletions insteon_mqtt/network/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
#===========================================================================
import errno
import itertools
import select
import time
from .. import log
Expand Down Expand Up @@ -57,6 +58,8 @@ def __init__(self):

# Map of fileno to Link objects.
self.links = {}
# List of links to only call poll() on.
self.poll_links = []

# List of unconnected link tuples (Link, time) where time is the time
# is the next time to try reconnecting the linnk.
Expand All @@ -71,6 +74,26 @@ def active(self):
"""
return len(self.links) + len(self.unconnected)

#-----------------------------------------------------------------------
def add_poll(self, link):
"""Add a Link that is only polled.
The input link does not need a file descriptor and only has to
support the poll() method from the Link class. The link closing
signal can be used to remove the link from the manager.
Args:
link (Link): Link object to add to the manager.
"""
LOG.debug("Polling link added: %s", link)
self.poll_links.append(link)

# Mirror the link signals from a regular link. We may never use
# these but the symmetry might come in handy later.
link.signal_closing.connect(self.poll_link_closing)
link.signal_connected.emit(link, True)

#-----------------------------------------------------------------------
def add(self, link, connected=True):
"""Add a Link to the manager.
Expand Down Expand Up @@ -222,8 +245,9 @@ def select(self, time_out=None):
# any kind. There are some cases where the MQTT client poll can
# trigger a close - I'm not sure exactly why but it's shown up in
# user reports. So copy the links before iterating since closing the
# link mods the dict which isn't allowed.
for link in list(self.links.values()):
# link mods the dict which isn't allowed. This isn't ideal but the
# number of links should be small so it probably doesn't matter.
for link in itertools.chain(list(self.links.values()), self.poll_links):
link.poll(t)

#-----------------------------------------------------------------------
Expand All @@ -248,6 +272,19 @@ def link_closing(self, link):
# no longer connected.
link.signal_connected.emit(link, False)

#-----------------------------------------------------------------------
def poll_link_closing(self, link):
"""Callback when a poll only link is closing.
Arg:
link (Link): The link that is closing.
"""
self.poll_links.remove(link)

# Emit the connected signal to let anyone else know that the link is
# no longer connected.
link.signal_connected.emit(link, False)

#-----------------------------------------------------------------------
def link_needs_write(self, link, needs_write):
"""Callback when a link write status changes state.
Expand Down
44 changes: 41 additions & 3 deletions insteon_mqtt/network/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#
#===========================================================================
import errno
import itertools
import select
import time
from .. import log
Expand Down Expand Up @@ -50,8 +51,10 @@ def __init__(self):
self.write = []
self.error = []

# Map of filno to Link objects.
# Map of fileno to Link objects.
self.links = {}
# List of links to only call poll() on.
self.poll_links = []

# List of unconnected link tuples (Link, time) where time is the time
# is the next time to try reconnecting the linnk.
Expand All @@ -66,8 +69,29 @@ def active(self):
"""
return len(self.links) + len(self.unconnected)

#-----------------------------------------------------------------------
def add_poll(self, link):
"""Add a Link that is only polled.
The input link does not need a file descriptor and only has to
support the poll() method from the Link class. The link closing
signal can be used to remove the link from the manager.
Args:
link (Link): Link object to add to the manager.
"""
LOG.debug("Polling link added: %s", link)
self.poll_links.append(link)

# Mirror the link signals from a regular link. We may never use
# these but the symmetry might come in handy later.
link.signal_closing.connect(self.poll_link_closing)
link.signal_connected.emit(link, True)

#-----------------------------------------------------------------------
def add(self, link, connected=True):

"""Add a Link to the manager.
To remove a link, call link.close().
Expand Down Expand Up @@ -216,8 +240,9 @@ def select(self, time_out=None):
# any kind. There are some cases where the MQTT client poll can
# trigger a close - I'm not sure exactly why but it's shown up in
# user reports. So copy the links before iterating since closing the
# link mods the dict which isn't allowed.
for link in list(self.links.values()):
# link mods the dict which isn't allowed. This isn't ideal but the
# number of links should be small so it probably doesn't matter.
for link in itertools.chain(list(self.links.values()), self.poll_links):
link.poll(t)

#-----------------------------------------------------------------------
Expand All @@ -242,6 +267,19 @@ def link_closing(self, link):
# no longer connected.
link.signal_connected.emit(link, False)

#-----------------------------------------------------------------------
def poll_link_closing(self, link):
"""Callback when a poll only link is closing.
Arg:
link (Link): The link that is closing.
"""
self.poll_links.remove(link)

# Emit the connected signal to let anyone else know that the link is
# no longer connected.
link.signal_connected.emit(link, False)

#-----------------------------------------------------------------------
def link_needs_write(self, link, needs_write):
"""Callback when a link write status changes state.
Expand Down

0 comments on commit 539b62f

Please sign in to comment.