8000 Better locking while setting up components + discovery by balloob · Pull Request #4463 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Better locking while setting up components + discovery #4463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging.handlers
import os
import sys
from collections import defaultdict
from collections import OrderedDict

from types import ModuleType
from typing import Any, Optional, Dict
Expand Down Expand Up @@ -57,7 +57,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str,
yield from hass.loop.run_in_executor(None, loader.prepare, hass)

if config is None:
config = defaultdict(dict)
config = {}

components = loader.load_order_component(domain)

Expand Down Expand Up @@ -345,7 +345,7 @@ def _async_init_from_config_dict(future):

# run task
future = asyncio.Future(loop=hass.loop)
hass.loop.create_task(_async_init_from_config_dict(future))
hass.async_add_job(_async_init_from_config_dict(future))
hass.loop.run_until_complete(future)

return future.result()
Expand All @@ -365,6 +365,12 @@ def async_from_config_dict(config: Dict[str, Any],
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
setup_lock = hass.data.get('setup_lock')
if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)

yield from setup_lock.acquire()

core_config = config.get(core.DOMAIN, {})

try:
Expand All @@ -388,10 +394,12 @@ def async_from_config_dict(config: Dict[str, Any],
yield from hass.loop.run_in_executor(None, loader.prepare, hass)

# Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict
# Use OrderedDict in case original one was one.
# Convert values to dictionaries if they are None
config = defaultdict(
dict, {key: value or {} for key, value in config.items()})
new_config = OrderedDict()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always thought that we setup everything with an ordered dict but looks like I was wrong. Instead we converted everything to a normal dictionary, making each setup unpredictable.

for key, value in config.items():
new_config[key] = value or {}
config = new_config

# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
Expand All @@ -417,6 +425,8 @@ def async_from_config_dict(config: Dict[str, Any],
for domain in loader.load_order_components(components):
yield from _async_setup_component(hass, domain, config)

setup_lock.release()

return hass


Expand Down
67 changes: 49 additions & 18 deletions homeassistant/helpers/discovery.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Helper methods to help with platform discovery."""
"""Helper methods to help with platform discovery.

There are two different types of discoveries that can be fired/listened for.
- listen/discover is for services. These are targetted at a component.
- listen_platform/discover_platform is for platforms. These are used by
components to allow discovery of their platofrms.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platofrms -> platforms

"""
import asyncio

from homeassistant import bootstrap, core
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
from homeassistant.util.async import (
run_callback_threadsafe, fire_coroutine_threadsafe)
from homeassistant.util.async import run_callback_threadsafe

EVENT_LOAD_PLATFORM = 'load_platform.{}'
ATTR_PLATFORM = 'platform'
Expand Down Expand Up @@ -43,8 +48,29 @@ def discovery_event_listener(event):

def discover(hass, service, discovered=None, component=None, hass_config=None):
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None:
bootstrap.setup_component(hass, component, hass_config)
hass.async_add_job(
async_discover(hass, service, discovered, component, hass_config))


@asyncio.coroutine
def async_discover(hass, service, discovered=None, component=None,
hass_config=None):
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None and component not in hass.config.components:
did_lock = False
setup_lock = hass.data.get('setup_lock')
if setup_lock and setup_lock.locked():
did_lock = True
yield from setup_lock.acquire()

try:
# Could have been loaded while waiting for lock.
if component not in hass.config.components:
yield from bootstrap.async_setup_component(hass, component,
hass_config)
finally:
if did_lock:
setup_lock.release()

data = {
ATTR_SERVICE: service
Expand All @@ -53,7 +79,7 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
if discovered is not None:
data[ATTR_DISCOVERED] = discovered

hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data)


def listen_platform(hass, component, callback):
Expand Down Expand Up @@ -101,9 +127,9 @@ def load_platform(hass, component, platform, discovered=None,

Use `listen_platform` to register a callback for these events.
"""
fire_coroutine_threadsafe(
async_load_platform(hass, component, platform,
discovered, hass_config), hass.loop)
hass.async_add_job(
async_load_platform(hass, component, platform, discovered,
hass_config))


@asyncio.coroutine
Expand All @@ -120,25 +146,30 @@ def async_load_platform(hass, component, platform, discovered=None,
Use `listen_platform` to register a callback for these events.

Warning: Do not yield from this inside a setup method to avoid a dead lock.
Use `hass.loop.create_task(async_load_platform(..))` instead.
Use `hass.loop.async_add_job(async_load_platform(..))` instead.

This method is a coroutine.
"""
did_lock = False
setup_lock = hass.data.get('setup_lock')
if setup_lock and setup_lock.locked():
did_lock = True
yield from setup_lock.acquire()
if component not in hass.config.components:
setup_lock = hass.data.get('setup_lock')
if setup_lock and setup_lock.locked():
did_lock = True
yield from setup_lock.acquire()

setup_success = True

try:
# No need to fire event if we could not setup component
res = yield from bootstrap.async_setup_component(
hass, component, hass_config)
# Could have been loaded while waiting for lock.
if component not in hass.config.components:
setup_success = yield from bootstrap.async_setup_component(
hass, component, hass_config)
finally:
if did_lock:
setup_lock.release()

if not res:
# No need to fire event if we could not setup component
if not setup_success:
return

data = {
Expand Down
56 changes: 51 additions & 5 deletions tests/helpers/test_discovery.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Test discovery helpers."""
from collections import OrderedDict
from unittest.mock import patch

from homeassistant import loader, bootstrap
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.util.async import run_coroutine_threadsafe

Expand All @@ -20,16 +22,18 @@ def teardown_method(self, method):
"""Stop everything that was started."""
self.hass.stop()

@patch('homeassistant.bootstrap.setup_component')
@patch('homeassistant.bootstrap.async_setup_component')
def test_listen(self, mock_setup_component):
"""Test discovery listen/discover combo."""
calls_single = []
calls_multi = []

@callback
def callback_single(service, info):
"""Service discovered callback."""
calls_single.append((service, info))

@callback
def callback_multi(service, info):
"""Service discovered callback."""
calls_multi.append((service, info))
Expand All @@ -42,16 +46,16 @@ def callback_multi(service, info):
'test_component')
self.hass.block_till_done()

discovery.discover(self.hass, 'another service', 'discovery info',
'test_component')
self.hass.block_till_done()

assert mock_setup_component.called
assert mock_setup_component.call_args[0] == \
(self.hass, 'test_component', None)
assert len(calls_single) == 1
assert calls_single[0] == ('test service', 'discovery info')

discovery.discover(self.hass, 'another service', 'discovery info',
'test_component')
self.hass.block_till_done()

assert len(calls_single) == 1
assert len(calls_multi) == 2
assert ['test service', 'another service'] == [info[0] for info
Expand All @@ -63,6 +67,7 @@ def test_platform(self, mock_setup_component):
"""Test discover platform method."""
calls = []

@callback
def platform_callback(platform, info):
"""Platform callback method."""
calls.append((platform, info))
Expand Down Expand Up @@ -149,3 +154,44 @@ def setup_platform(hass, config, add_devices_callback,
assert len(platform_calls) == 2
assert 'test_component' in self.hass.config.components
assert 'switch' in self.hass.config.components

def test_1st_discovers_2nd_component(self):
"""Test that we don't break if one component discovers the other.

If the first component fires a discovery event to setup the
second component while the second component is about to be setup,
it should not setup the second component twice.
"""
component_calls = []

def component1_setup(hass, config):
"""Setup mock component."""
discovery.discover(hass, 'test_component2')
return True

def component2_setup(hass, config):
"""Setup mock component."""
component_calls.append(1)
return True

loader.set_component(
'test_component1',
MockModule('test_component1', setup=component1_setup))

loader.set_component(
'test_component2',
MockModule('test_component2', setup=component2_setup))

config = OrderedDict()
config['test_component1'] = {}
config['test_component2'] = {}

self.hass.loop.run_until_complete = \
lambda _: self.hass.block_till_done()

bootstrap.from_config_dict(config, self.hass)

self.hass.block_till_done()

# test_component will only be setup once
assert len(component_calls) == 1
0