From 7ddc90edf2d74e92256f219a76bfcc073d9b4d5c Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Fri, 31 Jul 2020 13:28:40 +0100 Subject: [PATCH] scan: use non-blocking io to read the contact file --- cylc/flow/network/scan_nt.py | 4 +-- cylc/flow/suite_files.py | 43 +++++++++++++++++++------ setup.py | 1 + tests/integration/test_suite_files.py | 46 +++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 tests/integration/test_suite_files.py diff --git a/cylc/flow/network/scan_nt.py b/cylc/flow/network/scan_nt.py index a47440a83a0..6d89a99aaa3 100644 --- a/cylc/flow/network/scan_nt.py +++ b/cylc/flow/network/scan_nt.py @@ -37,7 +37,7 @@ ContactFileFields, SuiteFiles, get_suite_title, - load_contact_file + load_contact_file_async ) @@ -169,7 +169,7 @@ async def contact_info(flow): """ flow.update( - load_contact_file(flow['name'], path=flow['path']) + await load_contact_file_async(flow['name'], run_dir=flow['path']) ) return flow diff --git a/cylc/flow/suite_files.py b/cylc/flow/suite_files.py index 4b0007b387d..1588d3fc56b 100644 --- a/cylc/flow/suite_files.py +++ b/cylc/flow/suite_files.py @@ -17,20 +17,23 @@ # Note: Some modules are NOT imported in the header. Expensive modules are only # imported on demand. -from functools import lru_cache +from pathlib import Path import os import re import shutil -import stat import zmq.auth +import aiofiles + from cylc.flow import LOG from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.exceptions import SuiteServiceFileError -from cylc.flow.pathutil import get_remote_suite_run_dir, get_suite_run_dir -import cylc.flow.flags +from cylc.flow.pathutil import get_suite_run_dir from cylc.flow.hostuserutil import ( - get_host, get_user, is_remote, is_remote_host, is_remote_user) + get_user, + is_remote_host, + is_remote_user +) from cylc.flow.unicode_rules import SuiteNameValidator from enum import Enum @@ -375,13 +378,10 @@ def get_suite_srv_dir(reg, suite_owner=None): return os.path.join(run_d, SuiteFiles.Service.DIRNAME) -def load_contact_file(reg, path=None, owner=None, host=None): +def load_contact_file(reg, owner=None, host=None): """Load contact file. Return data as key=value dict.""" file_base = SuiteFiles.Service.CONTACT - if path: - path = path / SuiteFiles.Service.DIRNAME - else: - path = get_suite_srv_dir(reg) + path = get_suite_srv_dir(reg) file_content = _load_local_item(file_base, path) if file_content: data = {} @@ -393,6 +393,29 @@ def load_contact_file(reg, path=None, owner=None, host=None): raise SuiteServiceFileError("Couldn't load contact file") +async def load_contact_file_async(reg, run_dir=None): + if not run_dir: + path = Path( + get_suite_srv_dir(reg), + SuiteFiles.Service.CONTACT + ) + else: + path = Path( + run_dir, + SuiteFiles.Service.DIRNAME, + SuiteFiles.Service.CONTACT + ) + try: + async with aiofiles.open(path, mode='r') as cont: + data = {} + async for line in cont: + key, value = [item.strip() for item in line.split("=", 1)] + data[key] = value + return data + except IOError: + raise SuiteServiceFileError("Couldn't load contact file") + + def parse_suite_arg(options, arg): """From CLI arg "SUITE", return suite name and suite.rc path. diff --git a/setup.py b/setup.py index a4453e26491..a34549bfeef 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ def find_version(*file_paths): install_requires = [ + 'aiofiles==0.5.*', 'ansimarkup>=1.0.0', 'colorama>=0.4,<=1', 'click>=7.0', diff --git a/tests/integration/test_suite_files.py b/tests/integration/test_suite_files.py new file mode 100644 index 00000000000..5f1ec2cd9f3 --- /dev/null +++ b/tests/integration/test_suite_files.py @@ -0,0 +1,46 @@ +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pytest + +from cylc.flow.suite_files import ( + ContactFileFields as CFF, + load_contact_file, + load_contact_file_async +) + + +@pytest.fixture(scope='module') +async def myflow(mod_flow, mod_scheduler, mod_run, mod_one_conf): + reg = mod_flow(mod_one_conf) + schd = mod_scheduler(reg) + async with mod_run(schd): + yield schd + + +def test_load_contact_file(myflow): + cont = load_contact_file(myflow.suite) + assert cont[CFF.HOST] == myflow.host + + +@pytest.mark.asyncio +async def test_load_contact_file_async(myflow): + cont = await load_contact_file_async(myflow.suite) + assert cont[CFF.HOST] == myflow.host + + # compare the async interface to the sync interface + cont2 = load_contact_file(myflow.suite) + assert cont == cont2