Skip to content

Commit d5ac2e2

Browse files
psutil: allow accessing psutil object methods (#6733)
* The `_is_process_running` check was requesting the `psutil.Process` object via the `cylc psutil` command. * It would appear that these objects can get large enough to fill up the buffer. This means that `subprocess.Process.communicate` or `.wait` will hang indefinitely unless something reads from stdout to clear out the buffers. * This commit changes the `is_process_running` check to request only the one field it actually requires, `psutil.Process.cmdline` to reduce the data volume to a level where pipe polling is not required.
1 parent c984044 commit d5ac2e2

File tree

5 files changed

+52
-20
lines changed

5 files changed

+52
-20
lines changed

changes.d/6733.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an issue where the message "Cannot tell if the workflow is running" error could appear erroneously.

cylc/flow/scripts/psutil.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,47 @@ def get_option_parser():
4646
return OptionParser(__doc__)
4747

4848

49+
def _call_psutil_interface(interface, *args):
50+
"""Call a psutil interface with the provided arguments.
51+
52+
Args:
53+
interface:
54+
The psutil method we want to call, e.g. "cpu_percent".
55+
56+
In the case of objects, this may include attributes, e.g.
57+
"Process.cmdline".
58+
args:
59+
The arguments to provide to the psutil method.
60+
61+
Returns:
62+
The result of the psutil method call.
63+
64+
"""
65+
result = psutil
66+
is_first = True
67+
for fcn in interface.split('.'):
68+
try:
69+
method = getattr(result, fcn)
70+
except AttributeError as exc:
71+
# error obtaining interfaces from psutil e.g:
72+
# * requesting a method which does not exist
73+
print(exc, file=sys.stderr)
74+
sys.exit(2)
75+
76+
if is_first:
77+
is_first = False
78+
else:
79+
args = ()
80+
81+
result = method(*args)
82+
return result
83+
84+
4985
def _psutil(metrics_json):
5086
metrics = parse_dirty_json(metrics_json)
5187

5288
try:
53-
methods = [
54-
getattr(psutil, key[0])
55-
for key in metrics
56-
]
57-
except AttributeError as exc:
58-
# error obtaining interfaces from psutil e.g:
59-
# * requesting a method which does not exist
60-
print(exc, file=sys.stderr)
61-
sys.exit(2)
62-
63-
try:
64-
ret = [
65-
method(*key[1:])
66-
for key, method in zip(metrics, methods)
67-
]
89+
ret = [_call_psutil_interface(*key) for key in metrics]
6890
except Exception as exc:
6991
# error extracting metrics from psutil e.g:
7092
# * requesting information on a resource which does not exist

cylc/flow/workflow_files.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ def _is_process_running(
411411
from cylc.flow.terminal import parse_dirty_json
412412

413413
# See if the process is still running or not.
414-
metric = f'[["Process", {pid}]]'
414+
metric = f'[["Process.cmdline", {pid}]]'
415415
if is_remote_host(host):
416416
cmd = ['psutil']
417417
cmd = construct_cylc_server_ssh_cmd(cmd, host)
@@ -459,7 +459,7 @@ def _is_process_running(
459459
f'\n{command}'
460460
)
461461

462-
return cli_format(process['cmdline']) == command
462+
return cli_format(process) == command
463463

464464

465465
def detect_old_contact_file(

tests/integration/test_workflow_files.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,7 @@ def communicate(self, *args, **kwargs):
291291
# respond with something Cylc should be able to make sense of
292292
stdout = dedent('''
293293
% simulated stdout pollution %
294-
[{
295-
"cmdline": ["expected", "command"]
296-
}]
294+
[["expected", "command"]]
297295
''')
298296

299297
caplog.set_level(logging.WARN, logger=CYLC_LOG)

tests/unit/test_psutil.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import json
19+
import os
20+
21+
from psutil import Process
1922

2023
from cylc.flow.scripts.psutil import _psutil
2124

@@ -38,3 +41,11 @@ def test_psutil_basic():
3841
assert key in mem
3942
# all of which should be integers
4043
assert isinstance(mem[key], int)
44+
45+
46+
def test_psutil_object():
47+
"""It should call object methods."""
48+
# obtain the commandline for this test process
49+
ret = _psutil(f'[["Process.cmdline", {os.getpid()}]]')
50+
51+
assert ret[0] == Process().cmdline()

0 commit comments

Comments
 (0)