Skip to content

Commit

Permalink
host-select: rename thresholds -> ranking
Browse files Browse the repository at this point in the history
  • Loading branch information
oliver-sanders committed Mar 15, 2020
1 parent 1cd3881 commit 980c9f1
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 62 deletions.
2 changes: 1 addition & 1 deletion cylc/flow/cfgspec/globalcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
'run ports': [VDR.V_INTEGER_LIST, list(range(43001, 43101))],
'condemned hosts': [VDR.V_ABSOLUTE_HOST_LIST],
'auto restart delay': [VDR.V_INTERVAL],
'thresholds': [VDR.V_STRING]
'ranking': [VDR.V_STRING]
},
}

Expand Down
84 changes: 42 additions & 42 deletions cylc/flow/host_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def select_suite_host(cached=True):
"""Return a host as specified in `[suite hosts]`.
* Condemned hosts are filtered out.
* Filters by thresholds (if defined).
* Ranks by thresholds (if defined).
* Filters out hosts excluded by ranking (if defined).
* Ranks by ranking (if defined).
Args:
cached (bool):
Expand All @@ -43,8 +43,8 @@ def select_suite_host(cached=True):
return select_host(
# list of suite hosts
global_config.get(['suite servers', 'run hosts']) or ['localhost'],
# thresholds / ranking to apply
threshold_string=global_config.get(['suite servers', 'thresholds']),
# rankings to apply
ranking_string=global_config.get(['suite servers', 'ranking']),
# list of condemned hosts
blacklist=global_config.get(
['suite servers', 'condemned hosts']
Expand All @@ -55,21 +55,21 @@ def select_suite_host(cached=True):

def select_host(
hosts,
threshold_string=None,
ranking_string=None,
blacklist=None,
blacklist_name=None
):
"""Select a host from the provided list.
If no ranking is provided (in `threshold_string`) then random selection
If no ranking is provided (in `ranking_string`) then random selection
is used.
Args:
hosts (list):
List of host names to choose from.
NOTE: Host names must be identifyable from the host where the
call is executed.
threshold_string (str):
ranking_string (str):
A multiline string containing Python expressions to fileter
hosts by e.g::
Expand Down Expand Up @@ -130,20 +130,20 @@ def select_host(
# no hosts provided / left after filtering
raise HostSelectException(data)

thresholds = []
if threshold_string:
# parse thresholds
thresholds = list(_get_thresholds(threshold_string))
rankings = []
if ranking_string:
# parse rankings
rankings = list(_get_rankings(ranking_string))

if not thresholds:
if not rankings:
# no metrics or ranking required, pick host at random
hosts = [random.choice(list(hosts))] # nosec

if not thresholds and len(hosts) == 1:
if not rankings and len(hosts) == 1:
return hostname_map[hosts[0]], hosts[0]

# filter and sort by thresholds
metrics = list({x for x, _ in thresholds}) # required metrics
# filter and sort by rankings
metrics = list({x for x, _ in rankings}) # required metrics
results, data = _get_metrics( # get data from each host
hosts, metrics, data)
hosts = list(results) # some hosts might not be contactable
Expand All @@ -152,13 +152,13 @@ def select_host(
if not hosts:
# no hosts provided / left after filtering
raise HostSelectException(data)
if not thresholds and len(hosts) == 1:
if not rankings and len(hosts) == 1:
return hostname_map[hosts[0]], hosts[0]

hosts, data = _filter_by_threshold(
# filter by thresholds, sort by ranking
hosts, data = _filter_by_ranking(
# filter by rankings, sort by ranking
hosts,
thresholds,
rankings,
results,
data=data
)
Expand Down Expand Up @@ -211,15 +211,15 @@ def _filter_by_hostname(
return hosts, data


def _filter_by_threshold(hosts, thresholds, results, data=None):
"""Filter and rank by the provided thresholds.
def _filter_by_ranking(hosts, rankings, results, data=None):
"""Filter and rank by the provided rankings.
Args:
hosts (list):
List of host fqdns.
thresholds (list):
rankings (list):
Thresholds which must be met.
List of thresholds as returned by `get_thresholds`.
List of rankings as returned by `get_rankings`.
results (dict):
Nested dictionary as returned by `get_metrics` of the form:
`{host: {value: result, ...}, ...}`.
Expand All @@ -229,23 +229,23 @@ def _filter_by_threshold(hosts, thresholds, results, data=None):
Examples:
# ranking
>>> _filter_by_threshold(
>>> _filter_by_ranking(
... ['a', 'b'],
... [('X', 'RESULT')],
... {'a': {'X': 123}, 'b': {'X': 234}}
... )
(['a', 'b'], {'a': {}, 'b': {}})
# thresholds
>>> _filter_by_threshold(
# rankings
>>> _filter_by_ranking(
... ['a', 'b'],
... [('X', 'RESULT < 200')],
... {'a': {'X': 123}, 'b': {'X': 234}}
... )
(['a'], {'a': {'X() < 200': True}, 'b': {'X() < 200': False}})
# no matching hosts
>>> _filter_by_threshold(
>>> _filter_by_ranking(
... ['a'],
... [('X', 'RESULT > 1')],
... {'a': {'X': 0}}
Expand All @@ -257,17 +257,17 @@ def _filter_by_threshold(hosts, thresholds, results, data=None):
data = {host: {} for host in hosts}
good = []
for host in hosts:
host_thresholds = {}
host_rankings = {}
host_rank = []
for key, expression in thresholds:
for key, expression in rankings:
item = _reformat_expr(key, expression)
result = _simple_eval(expression, RESULT=results[host][key])
if isinstance(result, bool):
host_thresholds[item] = result
host_rankings[item] = result
data[host][item] = result
else:
host_rank.append(result)
if all(host_thresholds.values()):
if all(host_rankings.values()):
good.append((host_rank, host))

if not good:
Expand All @@ -280,7 +280,7 @@ def _filter_by_threshold(hosts, thresholds, results, data=None):
random.shuffle(good)

return (
# list of all hosts which passed thresholds (sorted by ranking)
# list of all hosts which passed rankings (sorted by ranking)
[host for _, host in good],
# data
data
Expand Down Expand Up @@ -352,24 +352,24 @@ def _simple_eval(expr, **variables):
raise ValueError(expr)


def _get_thresholds(string):
"""Yield parsed threshold expressions.
def _get_rankings(string):
"""Yield parsed ranking expressions.
Examples:
The first ``token.NAME`` encountered is returned as the query:
>>> _get_thresholds('foo() == 123').__next__()
>>> _get_rankings('foo() == 123').__next__()
(('foo',), 'RESULT == 123')
If multiple are present they will not get parsed:
>>> _get_thresholds('foo() in bar()').__next__()
>>> _get_rankings('foo() in bar()').__next__()
(('foo',), 'RESULT in bar()')
Positional arguments are added to the query tuple:
>>> _get_thresholds('1 in foo("a")').__next__()
>>> _get_rankings('1 in foo("a")').__next__()
(('foo', 'a'), '1 in RESULT')
Comments (not in-line) and multi-line strings are permitted:
>>> _get_thresholds('''
>>> _get_rankings('''
... # earl of sandwhich
... foo() == 123
... # beef wellington
Expand Down Expand Up @@ -534,13 +534,13 @@ def _get_metrics(hosts, metrics, data=None):


def _reformat_expr(key, expression):
"""Convert a threshold tuple back into an expression.
"""Convert a ranking tuple back into an expression.
Examples:
>>> threshold = 'a().b < c'
>>> ranking = 'a().b < c'
>>> _reformat_expr(
... *[x for x in _get_thresholds(threshold)][0]
... ) == threshold
... *[x for x in _get_rankings(ranking)][0]
... ) == ranking
True
"""
Expand Down
18 changes: 9 additions & 9 deletions cylc/flow/tests/test_host_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ def test_filter():
assert message in str(excinfo.value)


def test_thresholds():
"""Positive test that thresholds are evaluated.
def test_rankings():
"""Positive test that rankings are evaluated.
(doesn't prove anything by itself hence test_unreasonable_thresholds)
(doesn't prove anything by itself hence test_unreasonable_rankings)
"""
assert select_host(
[localhost],
threshold_string='''
ranking_string='''
# if this test fails due to race conditions
# then you have bigger issues than a test failure
virtual_memory().available > 1
Expand All @@ -76,15 +76,15 @@ def test_thresholds():
) == (localhost, localhost_fqdn)


def test_unreasonable_thresholds():
"""Negative test that thresholds are evaluated.
def test_unreasonable_rankings():
"""Negative test that rankings are evaluated.
(doesn't prove anything by itself hence test_thresholds)
(doesn't prove anything by itself hence test_rankings)
"""
with pytest.raises(HostSelectException) as excinfo:
select_host(
[localhost],
threshold_string='''
ranking_string='''
# if this test fails due to race conditions
# then you are very lucky
virtual_memory().available > 123456789123456789
Expand All @@ -103,7 +103,7 @@ def test_metric_command_failure():
with pytest.raises(HostSelectException) as excinfo:
select_host(
[localhost],
threshold_string='''
ranking_string='''
# elephant is not a psutil attribute
# so will cause the command to fail
elephant
Expand Down
20 changes: 10 additions & 10 deletions cylc/flow/tests/test_host_select_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ def test_remote_blacklict():
) == (local_host, local_host_fqdn)


def test_remote_thresholds():
"""Test that threshold evaluation works on remote hosts (via SSH)."""
def test_remote_rankings():
"""Test that ranking evaluation works on remote hosts (via SSH)."""
assert select_host(
[remote_host],
threshold_string='''
ranking_string='''
# if this test fails due to race conditions
# then you have bigger issues than a test failure
virtual_memory().available > 1
Expand All @@ -77,7 +77,7 @@ def test_remote_thresholds():


def test_remote_exclude(monkeypatch):
"""Ensure that hosts get excluded if they don't meet the thresholds.
"""Ensure that hosts get excluded if they don't meet the rankings.
Already tested elsewhere but this double-checks that it works if more
than one host is provided to choose from."""
Expand All @@ -93,7 +93,7 @@ def mocked_get_metrics(hosts, metrics, _=None):
)
assert select_host(
[local_host, remote_host],
threshold_string='''
ranking_string='''
cpu_count()
'''
) == (local_host, local_host_fqdn)
Expand Down Expand Up @@ -125,14 +125,14 @@ def test_remote_suite_host_condemned(mock_glbl_cfg):
assert select_suite_host() == (local_host, local_host_fqdn)


def test_remote_suite_host_thresholds(mock_glbl_cfg):
"""test [suite servers]thresholds"""
def test_remote_suite_host_rankings(mock_glbl_cfg):
"""test [suite servers]rankings"""
mock_glbl_cfg(
'cylc.flow.host_select.glbl_cfg',
f'''
[suite servers]
run hosts = {remote_host}
thresholds = """
rankings = """
# if this test fails due to race conditions
# then you are very lucky
virtual_memory().available > 123456789123456789
Expand All @@ -144,12 +144,12 @@ def test_remote_suite_host_thresholds(mock_glbl_cfg):
)
with pytest.raises(HostSelectException) as excinfo:
select_suite_host()
# ensure that host selection actually evuluated thresholds
# ensure that host selection actually evuluated rankings
assert set(excinfo.value.data[remote_host_fqdn]) == {
'virtual_memory().available > 123456789123456789',
'getloadavg()[0] < 1',
'cpu_count() > 512',
"disk_usage('/').free > 123456789123456789"
}
# ensure that none of the thresholds passed
# ensure that none of the rankings passed
assert not any(excinfo.value.data[remote_host_fqdn].values())

0 comments on commit 980c9f1

Please sign in to comment.