parsl 2024.6.3__py3-none-any.whl → 2024.6.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parsl/app/app.py +0 -2
- parsl/config.py +27 -4
- parsl/dataflow/dflow.py +36 -10
- parsl/executors/high_throughput/executor.py +36 -30
- parsl/executors/high_throughput/interchange.py +26 -28
- parsl/providers/kubernetes/kube.py +22 -9
- parsl/providers/slurm/slurm.py +31 -22
- parsl/tests/configs/flux_local.py +11 -0
- parsl/tests/conftest.py +4 -0
- parsl/tests/test_bash_apps/test_stdout.py +20 -2
- parsl/tests/test_htex/test_htex.py +24 -7
- parsl/tests/test_htex/test_zmq_binding.py +22 -6
- parsl/tests/test_python_apps/test_context_manager.py +96 -1
- parsl/tests/test_python_apps/test_dependencies_deep.py +59 -0
- parsl/tests/test_radical/test_mpi_funcs.py +0 -1
- parsl/tests/unit/test_usage_tracking.py +45 -0
- parsl/usage_tracking/levels.py +6 -0
- parsl/usage_tracking/usage.py +54 -23
- parsl/version.py +1 -1
- parsl-2024.6.17.data/scripts/interchange.py +681 -0
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/METADATA +2 -2
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/RECORD +29 -24
- {parsl-2024.6.3.data → parsl-2024.6.17.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2024.6.3.data → parsl-2024.6.17.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2024.6.3.data → parsl-2024.6.17.data}/scripts/process_worker_pool.py +0 -0
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/LICENSE +0 -0
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/WHEEL +0 -0
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/entry_points.txt +0 -0
- {parsl-2024.6.3.dist-info → parsl-2024.6.17.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,11 @@
|
|
1
1
|
import pathlib
|
2
2
|
import warnings
|
3
|
+
from subprocess import Popen, TimeoutExpired
|
3
4
|
from unittest import mock
|
4
5
|
|
5
6
|
import pytest
|
6
7
|
|
7
8
|
from parsl import HighThroughputExecutor, curvezmq
|
8
|
-
from parsl.multiprocessing import ForkProcess
|
9
9
|
|
10
10
|
_MOCK_BASE = "parsl.executors.high_throughput.executor"
|
11
11
|
|
@@ -78,16 +78,33 @@ def test_htex_shutdown(
|
|
78
78
|
timeout_expires: bool,
|
79
79
|
htex: HighThroughputExecutor,
|
80
80
|
):
|
81
|
-
mock_ix_proc = mock.Mock(spec=
|
81
|
+
mock_ix_proc = mock.Mock(spec=Popen)
|
82
82
|
|
83
83
|
if started:
|
84
84
|
htex.interchange_proc = mock_ix_proc
|
85
|
-
|
85
|
+
|
86
|
+
# This will, in the absence of any exit trigger, block forever if
|
87
|
+
# no timeout is given and if the interchange does not terminate.
|
88
|
+
# Raise an exception to report that, rather than actually block,
|
89
|
+
# and hope that nothing is catching that exception.
|
90
|
+
|
91
|
+
# this function implements the behaviour if the interchange has
|
92
|
+
# not received a termination call
|
93
|
+
def proc_wait_alive(timeout):
|
94
|
+
if timeout:
|
95
|
+
raise TimeoutExpired(cmd="mock-interchange", timeout=timeout)
|
96
|
+
else:
|
97
|
+
raise RuntimeError("This wait call would hang forever")
|
98
|
+
|
99
|
+
def proc_wait_terminated(timeout):
|
100
|
+
return 0
|
101
|
+
|
102
|
+
mock_ix_proc.wait.side_effect = proc_wait_alive
|
86
103
|
|
87
104
|
if not timeout_expires:
|
88
105
|
# Simulate termination of the Interchange process
|
89
106
|
def kill_interchange(*args, **kwargs):
|
90
|
-
mock_ix_proc.
|
107
|
+
mock_ix_proc.wait.side_effect = proc_wait_terminated
|
91
108
|
|
92
109
|
mock_ix_proc.terminate.side_effect = kill_interchange
|
93
110
|
|
@@ -96,8 +113,8 @@ def test_htex_shutdown(
|
|
96
113
|
mock_logs = mock_logger.info.call_args_list
|
97
114
|
if started:
|
98
115
|
assert mock_ix_proc.terminate.called
|
99
|
-
assert mock_ix_proc.
|
100
|
-
assert {"timeout": 10} == mock_ix_proc.
|
116
|
+
assert mock_ix_proc.wait.called
|
117
|
+
assert {"timeout": 10} == mock_ix_proc.wait.call_args[1]
|
101
118
|
if timeout_expires:
|
102
119
|
assert "Unable to terminate Interchange" in mock_logs[1][0][0]
|
103
120
|
assert mock_ix_proc.kill.called
|
@@ -105,7 +122,7 @@ def test_htex_shutdown(
|
|
105
122
|
assert "Finished" in mock_logs[-1][0][0]
|
106
123
|
else:
|
107
124
|
assert not mock_ix_proc.terminate.called
|
108
|
-
assert not mock_ix_proc.
|
125
|
+
assert not mock_ix_proc.wait.called
|
109
126
|
assert "has not started" in mock_logs[0][0][0]
|
110
127
|
|
111
128
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import pathlib
|
2
3
|
from typing import Optional
|
3
4
|
from unittest import mock
|
@@ -10,6 +11,21 @@ from parsl import curvezmq
|
|
10
11
|
from parsl.executors.high_throughput.interchange import Interchange
|
11
12
|
|
12
13
|
|
14
|
+
def make_interchange(*, interchange_address: Optional[str], cert_dir: Optional[str]) -> Interchange:
|
15
|
+
return Interchange(interchange_address=interchange_address,
|
16
|
+
cert_dir=cert_dir,
|
17
|
+
client_address="127.0.0.1",
|
18
|
+
client_ports=(50055, 50056, 50057),
|
19
|
+
worker_ports=None,
|
20
|
+
worker_port_range=(54000, 55000),
|
21
|
+
hub_address=None,
|
22
|
+
hub_zmq_port=None,
|
23
|
+
heartbeat_threshold=60,
|
24
|
+
logdir=".",
|
25
|
+
logging_level=logging.INFO,
|
26
|
+
poll_period=10)
|
27
|
+
|
28
|
+
|
13
29
|
@pytest.fixture
|
14
30
|
def encrypted(request: pytest.FixtureRequest):
|
15
31
|
if hasattr(request, "param"):
|
@@ -31,7 +47,7 @@ def test_interchange_curvezmq_sockets(
|
|
31
47
|
mock_socket: mock.MagicMock, cert_dir: Optional[str], encrypted: bool
|
32
48
|
):
|
33
49
|
address = "127.0.0.1"
|
34
|
-
ix =
|
50
|
+
ix = make_interchange(interchange_address=address, cert_dir=cert_dir)
|
35
51
|
assert isinstance(ix.zmq_context, curvezmq.ServerContext)
|
36
52
|
assert ix.zmq_context.encrypted is encrypted
|
37
53
|
assert mock_socket.call_count == 5
|
@@ -40,7 +56,7 @@ def test_interchange_curvezmq_sockets(
|
|
40
56
|
@pytest.mark.local
|
41
57
|
@pytest.mark.parametrize("encrypted", (True, False), indirect=True)
|
42
58
|
def test_interchange_binding_no_address(cert_dir: Optional[str]):
|
43
|
-
ix =
|
59
|
+
ix = make_interchange(interchange_address=None, cert_dir=cert_dir)
|
44
60
|
assert ix.interchange_address == "*"
|
45
61
|
|
46
62
|
|
@@ -49,7 +65,7 @@ def test_interchange_binding_no_address(cert_dir: Optional[str]):
|
|
49
65
|
def test_interchange_binding_with_address(cert_dir: Optional[str]):
|
50
66
|
# Using loopback address
|
51
67
|
address = "127.0.0.1"
|
52
|
-
ix =
|
68
|
+
ix = make_interchange(interchange_address=address, cert_dir=cert_dir)
|
53
69
|
assert ix.interchange_address == address
|
54
70
|
|
55
71
|
|
@@ -60,7 +76,7 @@ def test_interchange_binding_with_non_ipv4_address(cert_dir: Optional[str]):
|
|
60
76
|
# Confirm that a ipv4 address is required
|
61
77
|
address = "localhost"
|
62
78
|
with pytest.raises(zmq.error.ZMQError):
|
63
|
-
|
79
|
+
make_interchange(interchange_address=address, cert_dir=cert_dir)
|
64
80
|
|
65
81
|
|
66
82
|
@pytest.mark.local
|
@@ -69,7 +85,7 @@ def test_interchange_binding_bad_address(cert_dir: Optional[str]):
|
|
69
85
|
"""Confirm that we raise a ZMQError when a bad address is supplied"""
|
70
86
|
address = "550.0.0.0"
|
71
87
|
with pytest.raises(zmq.error.ZMQError):
|
72
|
-
|
88
|
+
make_interchange(interchange_address=address, cert_dir=cert_dir)
|
73
89
|
|
74
90
|
|
75
91
|
@pytest.mark.local
|
@@ -77,7 +93,7 @@ def test_interchange_binding_bad_address(cert_dir: Optional[str]):
|
|
77
93
|
def test_limited_interface_binding(cert_dir: Optional[str]):
|
78
94
|
"""When address is specified the worker_port would be bound to it rather than to 0.0.0.0"""
|
79
95
|
address = "127.0.0.1"
|
80
|
-
ix =
|
96
|
+
ix = make_interchange(interchange_address=address, cert_dir=cert_dir)
|
81
97
|
ix.worker_result_port
|
82
98
|
proc = psutil.Process()
|
83
99
|
conns = proc.connections(kind="tcp")
|
@@ -1,7 +1,11 @@
|
|
1
|
+
from concurrent.futures import Future
|
2
|
+
from threading import Event
|
3
|
+
|
1
4
|
import pytest
|
2
5
|
|
3
6
|
import parsl
|
4
|
-
from parsl.
|
7
|
+
from parsl.config import Config
|
8
|
+
from parsl.dataflow.dflow import DataFlowKernel, DataFlowKernelLoader
|
5
9
|
from parsl.errors import NoDataFlowKernelError
|
6
10
|
from parsl.tests.configs.local_threads import fresh_config
|
7
11
|
|
@@ -16,6 +20,16 @@ def foo(x, stdout='foo.stdout'):
|
|
16
20
|
return f"echo {x + 1}"
|
17
21
|
|
18
22
|
|
23
|
+
@parsl.python_app
|
24
|
+
def wait_for_event(ev: Event):
|
25
|
+
ev.wait()
|
26
|
+
|
27
|
+
|
28
|
+
@parsl.python_app
|
29
|
+
def raise_app():
|
30
|
+
raise RuntimeError("raise_app deliberate failure")
|
31
|
+
|
32
|
+
|
19
33
|
@pytest.mark.local
|
20
34
|
def test_within_context_manger(tmpd_cwd):
|
21
35
|
config = fresh_config()
|
@@ -31,3 +45,84 @@ def test_within_context_manger(tmpd_cwd):
|
|
31
45
|
with pytest.raises(NoDataFlowKernelError) as excinfo:
|
32
46
|
square(2).result()
|
33
47
|
assert str(excinfo.value) == "Must first load config"
|
48
|
+
|
49
|
+
|
50
|
+
@pytest.mark.local
|
51
|
+
def test_exit_skip():
|
52
|
+
config = fresh_config()
|
53
|
+
config.exit_mode = "skip"
|
54
|
+
|
55
|
+
with parsl.load(config) as dfk:
|
56
|
+
ev = Event()
|
57
|
+
fut = wait_for_event(ev)
|
58
|
+
# deliberately don't wait for this to finish, so that the context
|
59
|
+
# manager can exit
|
60
|
+
|
61
|
+
assert parsl.dfk() is dfk, "global dfk should be left in place by skip mode"
|
62
|
+
|
63
|
+
assert not fut.done(), "wait_for_event should not be done yet"
|
64
|
+
ev.set()
|
65
|
+
|
66
|
+
# now we can wait for that result...
|
67
|
+
fut.result()
|
68
|
+
assert fut.done(), "wait_for_event should complete outside of context manager in 'skip' mode"
|
69
|
+
|
70
|
+
# now cleanup the DFK that the above `with` block
|
71
|
+
# deliberately avoided doing...
|
72
|
+
dfk.cleanup()
|
73
|
+
|
74
|
+
|
75
|
+
# 'wait' mode has two cases to test:
|
76
|
+
# 1. that we wait when there is no exception
|
77
|
+
# 2. that we do not wait when there is an exception
|
78
|
+
@pytest.mark.local
|
79
|
+
def test_exit_wait_no_exception():
|
80
|
+
config = fresh_config()
|
81
|
+
config.exit_mode = "wait"
|
82
|
+
|
83
|
+
with parsl.load(config) as dfk:
|
84
|
+
fut = square(1)
|
85
|
+
# deliberately don't wait for this to finish, so that the context
|
86
|
+
# manager can exit
|
87
|
+
|
88
|
+
assert fut.done(), "This future should be marked as done before the context manager exits"
|
89
|
+
|
90
|
+
assert dfk.cleanup_called, "The DFK should have been cleaned up by the context manager"
|
91
|
+
assert DataFlowKernelLoader._dfk is None, "The global DFK should have been removed"
|
92
|
+
|
93
|
+
|
94
|
+
@pytest.mark.local
|
95
|
+
def test_exit_wait_exception():
|
96
|
+
config = fresh_config()
|
97
|
+
config.exit_mode = "wait"
|
98
|
+
|
99
|
+
with pytest.raises(RuntimeError):
|
100
|
+
with parsl.load(config) as dfk:
|
101
|
+
# we'll never fire this future
|
102
|
+
fut_never = Future()
|
103
|
+
|
104
|
+
fut_raise = raise_app()
|
105
|
+
|
106
|
+
fut_depend = square(fut_never)
|
107
|
+
|
108
|
+
# this should cause an exception, which should cause the context
|
109
|
+
# manager to exit, without waiting for fut_depend to finish.
|
110
|
+
fut_raise.result()
|
111
|
+
|
112
|
+
assert dfk.cleanup_called, "The DFK should have been cleaned up by the context manager"
|
113
|
+
assert DataFlowKernelLoader._dfk is None, "The global DFK should have been removed"
|
114
|
+
assert fut_raise.exception() is not None, "fut_raise should contain an exception"
|
115
|
+
assert not fut_depend.done(), "fut_depend should have been left un-done (due to dependency failure)"
|
116
|
+
|
117
|
+
|
118
|
+
@pytest.mark.local
|
119
|
+
def test_exit_wrong_mode():
|
120
|
+
|
121
|
+
with pytest.raises(Exception) as ex:
|
122
|
+
Config(exit_mode="wrongmode")
|
123
|
+
|
124
|
+
# with typeguard 4.x this is TypeCheckError,
|
125
|
+
# with typeguard 2.x this is TypeError
|
126
|
+
# we can't instantiate TypeCheckError if we're in typeguard 2.x environment
|
127
|
+
# because it does not exist... so check name using strings.
|
128
|
+
assert ex.type.__name__ == "TypeCheckError" or ex.type.__name__ == "TypeError"
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import inspect
|
2
|
+
from concurrent.futures import Future
|
3
|
+
from typing import Any, Callable, Dict
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
import parsl
|
8
|
+
from parsl.executors.base import ParslExecutor
|
9
|
+
|
10
|
+
# N is the number of tasks to chain
|
11
|
+
# With mid-2024 Parsl, N>140 causes Parsl to hang
|
12
|
+
N = 100
|
13
|
+
|
14
|
+
# MAX_STACK is the maximum Python stack depth allowed for either
|
15
|
+
# task submission to an executor or execution of a task.
|
16
|
+
# With mid-2024 Parsl, 2-3 stack entries will be used per
|
17
|
+
# recursively launched parsl task. So this should be smaller than
|
18
|
+
# 2*N, but big enough to allow regular pytest+parsl stuff to
|
19
|
+
# happen.
|
20
|
+
MAX_STACK = 50
|
21
|
+
|
22
|
+
|
23
|
+
def local_config():
|
24
|
+
return parsl.Config(executors=[ImmediateExecutor()])
|
25
|
+
|
26
|
+
|
27
|
+
class ImmediateExecutor(ParslExecutor):
|
28
|
+
def start(self):
|
29
|
+
pass
|
30
|
+
|
31
|
+
def shutdown(self):
|
32
|
+
pass
|
33
|
+
|
34
|
+
def submit(self, func: Callable, resource_specification: Dict[str, Any], *args: Any, **kwargs: Any) -> Future:
|
35
|
+
stack_depth = len(inspect.stack())
|
36
|
+
assert stack_depth < MAX_STACK, "tasks should not be launched deep in the Python stack"
|
37
|
+
fut: Future[None] = Future()
|
38
|
+
res = func(*args, **kwargs)
|
39
|
+
fut.set_result(res)
|
40
|
+
return fut
|
41
|
+
|
42
|
+
|
43
|
+
@parsl.python_app
|
44
|
+
def chain(upstream):
|
45
|
+
stack_depth = len(inspect.stack())
|
46
|
+
assert stack_depth < MAX_STACK, "chained dependencies should not be launched deep in the Python stack"
|
47
|
+
|
48
|
+
|
49
|
+
@pytest.mark.local
|
50
|
+
def test_deep_dependency_stack_depth():
|
51
|
+
|
52
|
+
fut = Future()
|
53
|
+
here = fut
|
54
|
+
|
55
|
+
for _ in range(N):
|
56
|
+
here = chain(here)
|
57
|
+
|
58
|
+
fut.set_result(None)
|
59
|
+
here.result()
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""Test usage_tracking values."""
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
import parsl
|
6
|
+
from parsl.config import Config
|
7
|
+
from parsl.errors import ConfigurationError
|
8
|
+
|
9
|
+
|
10
|
+
@pytest.mark.local
|
11
|
+
def test_config_load():
|
12
|
+
"""Test loading a config with usage tracking."""
|
13
|
+
with parsl.load(Config(usage_tracking=3)):
|
14
|
+
pass
|
15
|
+
parsl.clear()
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.mark.local
|
19
|
+
@pytest.mark.parametrize("level", (0, 1, 2, 3, False, True))
|
20
|
+
def test_valid(level):
|
21
|
+
"""Test valid usage_tracking values."""
|
22
|
+
Config(usage_tracking=level)
|
23
|
+
assert Config(usage_tracking=level).usage_tracking == level
|
24
|
+
|
25
|
+
|
26
|
+
@pytest.mark.local
|
27
|
+
@pytest.mark.parametrize("level", (12, 1000, -1))
|
28
|
+
def test_invalid_values(level):
|
29
|
+
"""Test invalid usage_tracking values."""
|
30
|
+
with pytest.raises(ConfigurationError):
|
31
|
+
Config(usage_tracking=level)
|
32
|
+
|
33
|
+
|
34
|
+
@pytest.mark.local
|
35
|
+
@pytest.mark.parametrize("level", ("abcd", None, bytes(1), 1.0, 1j, object()))
|
36
|
+
def test_invalid_types(level):
|
37
|
+
"""Test invalid usage_tracking types."""
|
38
|
+
with pytest.raises(Exception) as ex:
|
39
|
+
Config(usage_tracking=level)
|
40
|
+
|
41
|
+
# with typeguard 4.x this is TypeCheckError,
|
42
|
+
# with typeguard 2.x this is TypeError
|
43
|
+
# we can't instantiate TypeCheckError if we're in typeguard 2.x environment
|
44
|
+
# because it does not exist... so check name using strings.
|
45
|
+
assert ex.type.__name__ in ["TypeCheckError", "TypeError"]
|
@@ -0,0 +1,6 @@
|
|
1
|
+
"""Module for defining the usage tracking levels."""
|
2
|
+
|
3
|
+
DISABLED = 0 # Tracking is disabled
|
4
|
+
LEVEL_1 = 1 # Share info about Parsl version, Python version, platform
|
5
|
+
LEVEL_2 = 2 # Share info about config + level 1
|
6
|
+
LEVEL_3 = 3 # Share info about app count, app fails, execution time + level 2
|
parsl/usage_tracking/usage.py
CHANGED
@@ -7,8 +7,11 @@ import time
|
|
7
7
|
import uuid
|
8
8
|
|
9
9
|
from parsl.dataflow.states import States
|
10
|
+
from parsl.errors import ConfigurationError
|
10
11
|
from parsl.multiprocessing import ForkProcess
|
11
12
|
from parsl.usage_tracking.api import get_parsl_usage
|
13
|
+
from parsl.usage_tracking.levels import DISABLED as USAGE_TRACKING_DISABLED
|
14
|
+
from parsl.usage_tracking.levels import LEVEL_3 as USAGE_TRACKING_LEVEL_3
|
12
15
|
from parsl.utils import setproctitle
|
13
16
|
from parsl.version import VERSION as PARSL_VERSION
|
14
17
|
|
@@ -110,17 +113,32 @@ class UsageTracker:
|
|
110
113
|
self.python_version = "{}.{}.{}".format(sys.version_info.major,
|
111
114
|
sys.version_info.minor,
|
112
115
|
sys.version_info.micro)
|
113
|
-
self.
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
116
|
+
self.tracking_level = self.check_tracking_level()
|
117
|
+
self.start_time = None
|
118
|
+
logger.debug("Tracking level: {}".format(self.tracking_level))
|
119
|
+
|
120
|
+
def check_tracking_level(self) -> int:
|
121
|
+
"""Check if tracking is enabled and return level.
|
122
|
+
|
123
|
+
Checks usage_tracking in Config
|
124
|
+
- Possible values: [True, False, 0, 1, 2, 3]
|
125
|
+
|
126
|
+
True/False values are treated as Level 1/Level 0 respectively.
|
127
|
+
|
128
|
+
Returns: int
|
129
|
+
- 0 : Tracking is disabled
|
130
|
+
- 1 : Tracking is enabled with level 1
|
131
|
+
Share info about Parsl version, Python version, platform
|
132
|
+
- 2 : Tracking is enabled with level 2
|
133
|
+
Share info about config + level 1
|
134
|
+
- 3 : Tracking is enabled with level 3
|
135
|
+
Share info about app count, app fails, execution time + level 2
|
123
136
|
"""
|
137
|
+
if not USAGE_TRACKING_DISABLED <= self.config.usage_tracking <= USAGE_TRACKING_LEVEL_3:
|
138
|
+
raise ConfigurationError(
|
139
|
+
f"Usage Tracking values must be 0, 1, 2, or 3 and not {self.config.usage_tracking}"
|
140
|
+
)
|
141
|
+
|
124
142
|
return self.config.usage_tracking
|
125
143
|
|
126
144
|
def construct_start_message(self) -> bytes:
|
@@ -133,18 +151,28 @@ class UsageTracker:
|
|
133
151
|
'parsl_v': self.parsl_version,
|
134
152
|
'python_v': self.python_version,
|
135
153
|
'platform.system': platform.system(),
|
136
|
-
'
|
137
|
-
|
154
|
+
'tracking_level': int(self.tracking_level)}
|
155
|
+
|
156
|
+
if self.tracking_level >= 2:
|
157
|
+
message['components'] = get_parsl_usage(self.dfk._config)
|
158
|
+
|
159
|
+
if self.tracking_level == 3:
|
160
|
+
self.start_time = int(time.time())
|
161
|
+
message['start'] = self.start_time
|
162
|
+
|
138
163
|
logger.debug(f"Usage tracking start message: {message}")
|
139
164
|
|
140
165
|
return self.encode_message(message)
|
141
166
|
|
142
167
|
def construct_end_message(self) -> bytes:
|
143
168
|
"""Collect the final run information at the time of DFK cleanup.
|
169
|
+
This is only called if tracking level is 3.
|
144
170
|
|
145
171
|
Returns:
|
146
172
|
- Message dict dumped as json string, ready for UDP
|
147
173
|
"""
|
174
|
+
end_time = int(time.time())
|
175
|
+
|
148
176
|
app_count = self.dfk.task_count
|
149
177
|
|
150
178
|
app_fails = self.dfk.task_state_counts[States.failed] + self.dfk.task_state_counts[States.dep_fail]
|
@@ -157,7 +185,8 @@ class UsageTracker:
|
|
157
185
|
'app_fails': app_fails}
|
158
186
|
|
159
187
|
message = {'correlator': self.correlator_uuid,
|
160
|
-
'end':
|
188
|
+
'end': end_time,
|
189
|
+
'execution_time': end_time - self.start_time,
|
161
190
|
'components': [dfk_component] + get_parsl_usage(self.dfk._config)}
|
162
191
|
logger.debug(f"Usage tracking end message (unencoded): {message}")
|
163
192
|
|
@@ -168,20 +197,22 @@ class UsageTracker:
|
|
168
197
|
|
169
198
|
def send_UDP_message(self, message: bytes) -> None:
|
170
199
|
"""Send UDP message."""
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
logger.debug("Usage tracking failed: {}".format(e))
|
200
|
+
try:
|
201
|
+
proc = udp_messenger(self.domain_name, self.UDP_PORT, self.sock_timeout, message)
|
202
|
+
self.procs.append(proc)
|
203
|
+
except Exception as e:
|
204
|
+
logger.debug("Usage tracking failed: {}".format(e))
|
177
205
|
|
178
206
|
def send_start_message(self) -> None:
|
179
|
-
|
180
|
-
|
207
|
+
if self.tracking_level:
|
208
|
+
self.start_time = time.time()
|
209
|
+
message = self.construct_start_message()
|
210
|
+
self.send_UDP_message(message)
|
181
211
|
|
182
212
|
def send_end_message(self) -> None:
|
183
|
-
|
184
|
-
|
213
|
+
if self.tracking_level == 3:
|
214
|
+
message = self.construct_end_message()
|
215
|
+
self.send_UDP_message(message)
|
185
216
|
|
186
217
|
def close(self, timeout: float = 10.0) -> None:
|
187
218
|
"""First give each process one timeout period to finish what it is
|
parsl/version.py
CHANGED