mccode-plumber 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {mccode_plumber-0.6.0/src/mccode_plumber.egg-info → mccode_plumber-0.7.0}/PKG-INFO +3 -2
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/pyproject.toml +2 -1
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/CommandChannel.py +236 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/CommandHandler.py +58 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/CommandStatus.py +151 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/InThreadStatusTracker.py +228 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/JobHandler.py +102 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/JobStatus.py +147 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/KafkaTopicUrl.py +22 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/StateExtractor.py +58 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/WorkerFinder.py +139 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/WorkerJobPool.py +70 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/WorkerStatus.py +88 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/WriteJob.py +83 -0
- mccode_plumber-0.7.0/src/mccode_plumber/file_writer_control/__init__.py +13 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/writer.py +3 -3
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0/src/mccode_plumber.egg-info}/PKG-INFO +3 -2
- mccode_plumber-0.7.0/src/mccode_plumber.egg-info/SOURCES.txt +36 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber.egg-info/requires.txt +2 -1
- mccode_plumber-0.6.0/src/mccode_plumber.egg-info/SOURCES.txt +0 -23
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/.github/workflows/pip.yml +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/.github/workflows/wheels.yml +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/.gitignore +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/README.md +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/setup.cfg +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/__init__.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/conductor.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/epics.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/forwarder.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/kafka.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/mccode.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/splitrun.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber/utils.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber.egg-info/dependency_links.txt +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber.egg-info/entry_points.txt +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/src/mccode_plumber.egg-info/top_level.txt +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/tests/test_epics.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/tests/test_splitrun.py +0 -0
- {mccode_plumber-0.6.0 → mccode_plumber-0.7.0}/tests/test_writer.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mccode-plumber
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Author-email: Gregory Tucker <gregory.tucker@ess.eu>
|
|
5
5
|
Classifier: License :: OSI Approved :: BSD License
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: p4p
|
|
8
|
-
Requires-Dist:
|
|
8
|
+
Requires-Dist: kafka-python>=2.0
|
|
9
|
+
Requires-Dist: ess-streaming-data-types>=0.14.0
|
|
9
10
|
Requires-Dist: restage>=0.4.0
|
|
10
11
|
Requires-Dist: mccode-to-kafka>=0.2.1
|
|
11
12
|
Requires-Dist: moreniius>=0.2.3
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import threading
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from typing import Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from kafka import KafkaConsumer
|
|
8
|
+
from kafka.errors import NoBrokersAvailable
|
|
9
|
+
|
|
10
|
+
from file_writer_control.CommandStatus import CommandState, CommandStatus
|
|
11
|
+
from file_writer_control.InThreadStatusTracker import (
|
|
12
|
+
DEAD_ENTITY_TIME_LIMIT,
|
|
13
|
+
InThreadStatusTracker,
|
|
14
|
+
)
|
|
15
|
+
from file_writer_control.JobStatus import JobStatus
|
|
16
|
+
from file_writer_control.KafkaTopicUrl import KafkaTopicUrl
|
|
17
|
+
from file_writer_control.WorkerStatus import WorkerStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def thread_function(
|
|
21
|
+
host_port: str,
|
|
22
|
+
topic: str,
|
|
23
|
+
in_queue: Queue,
|
|
24
|
+
out_queue: Queue,
|
|
25
|
+
kafka_config: Dict[str, str] = {},
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Background thread for consuming Kafka messages.
|
|
29
|
+
:param host_port: The host + port of the Kafka broker that we are using.
|
|
30
|
+
:param topic: The Kafka topic that we are listening to.
|
|
31
|
+
:param in_queue: A queue for sending "exit" messages to the thread.
|
|
32
|
+
.. note:: The queue will exit upon the reception of the string "exit" on this queue.
|
|
33
|
+
:param out_queue: The queue to which status updates are published.
|
|
34
|
+
"""
|
|
35
|
+
status_tracker = InThreadStatusTracker(out_queue)
|
|
36
|
+
while True:
|
|
37
|
+
try:
|
|
38
|
+
consumer = KafkaConsumer(
|
|
39
|
+
topic,
|
|
40
|
+
bootstrap_servers=host_port,
|
|
41
|
+
fetch_max_bytes=52428800 * 6,
|
|
42
|
+
max_partition_fetch_bytes=52428800 * 10,
|
|
43
|
+
consumer_timeout_ms=100,
|
|
44
|
+
**kafka_config
|
|
45
|
+
) # Roughly 300MB
|
|
46
|
+
break
|
|
47
|
+
except NoBrokersAvailable:
|
|
48
|
+
pass # Do not fail if the broker is not immediately available.
|
|
49
|
+
if not in_queue.empty():
|
|
50
|
+
new_msg = in_queue.get()
|
|
51
|
+
if new_msg == "exit":
|
|
52
|
+
return
|
|
53
|
+
while True:
|
|
54
|
+
for message in consumer:
|
|
55
|
+
status_tracker.process_message(message.value)
|
|
56
|
+
status_tracker.check_for_lost_connections()
|
|
57
|
+
if not in_queue.empty():
|
|
58
|
+
new_msg = in_queue.get()
|
|
59
|
+
if new_msg == "exit":
|
|
60
|
+
break
|
|
61
|
+
consumer.close(True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CommandChannel(object):
|
|
65
|
+
"""
|
|
66
|
+
A class that implements the functionality for receiving and interpreting messages that are published to the
|
|
67
|
+
Kafka command topic of a pool of file-writers.
|
|
68
|
+
.. note:: This class implements a thread that will continuously attempt to connect to a Kafka broker.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, command_topic_url: str, kafka_config: Dict[str, str] = {}):
|
|
72
|
+
"""
|
|
73
|
+
Constructor.
|
|
74
|
+
:param command_topic_url: The url of the Kafka topic to where the file-writer status/command messages are published.
|
|
75
|
+
"""
|
|
76
|
+
kafka_address = KafkaTopicUrl(command_topic_url)
|
|
77
|
+
self.status_queue = Queue()
|
|
78
|
+
self.to_thread_queue = Queue()
|
|
79
|
+
thread_kwargs = {
|
|
80
|
+
"host_port": kafka_address.host_port,
|
|
81
|
+
"topic": kafka_address.topic,
|
|
82
|
+
"in_queue": self.to_thread_queue,
|
|
83
|
+
"out_queue": self.status_queue,
|
|
84
|
+
"kafka_config": kafka_config,
|
|
85
|
+
}
|
|
86
|
+
self.map_of_workers: Dict[str, WorkerStatus] = {}
|
|
87
|
+
self.map_of_jobs: Dict[str, JobStatus] = {}
|
|
88
|
+
self.map_of_commands: Dict[str, CommandStatus] = {}
|
|
89
|
+
self.run_thread = True
|
|
90
|
+
self.thread = threading.Thread(
|
|
91
|
+
target=thread_function, daemon=True, kwargs=thread_kwargs
|
|
92
|
+
)
|
|
93
|
+
self.thread.start()
|
|
94
|
+
|
|
95
|
+
def do_exit():
|
|
96
|
+
self.stop_thread()
|
|
97
|
+
|
|
98
|
+
atexit.register(do_exit)
|
|
99
|
+
|
|
100
|
+
def add_job_id(self, job_id: str):
|
|
101
|
+
"""
|
|
102
|
+
Add a job identifier to the list of known jobs before it has been encountered on the command topic.
|
|
103
|
+
:param job_id: The identifier of the new job.
|
|
104
|
+
"""
|
|
105
|
+
if job_id not in self.map_of_jobs:
|
|
106
|
+
self.map_of_jobs[job_id] = JobStatus(job_id)
|
|
107
|
+
|
|
108
|
+
def add_command_id(self, job_id: str, command_id: str):
|
|
109
|
+
"""
|
|
110
|
+
Add a command identifier to the list of known commands before it has been encountered on the command topic.
|
|
111
|
+
:param job_id: The job identifier of the new command.
|
|
112
|
+
:param command_id: The identifier of the new command.
|
|
113
|
+
"""
|
|
114
|
+
if command_id not in self.map_of_commands:
|
|
115
|
+
self.map_of_commands[command_id] = CommandStatus(job_id, command_id)
|
|
116
|
+
self.map_of_commands[command_id].state = CommandState.WAITING_RESPONSE
|
|
117
|
+
|
|
118
|
+
def stop_thread(self):
|
|
119
|
+
"""
|
|
120
|
+
Stop the thread that is continuously getting command topic messages in the background. Should only be called if
|
|
121
|
+
we are about to get rid of the current instance of CommandChannel.
|
|
122
|
+
"""
|
|
123
|
+
self.to_thread_queue.put("exit")
|
|
124
|
+
try:
|
|
125
|
+
self.thread.join()
|
|
126
|
+
except RuntimeError:
|
|
127
|
+
pass # Do not throw an exception if the thread has not yet been started.
|
|
128
|
+
|
|
129
|
+
def __del__(self):
|
|
130
|
+
self.stop_thread()
|
|
131
|
+
|
|
132
|
+
def update_workers(self, current_time: Optional[datetime] = None):
|
|
133
|
+
"""
|
|
134
|
+
Update the list of known workers, jobs and commands. This is a non-blocking call but it might take some time
|
|
135
|
+
to execute if the queue of updates is long. This member function is called by many of the other member functions
|
|
136
|
+
in this class.
|
|
137
|
+
"""
|
|
138
|
+
if current_time is None:
|
|
139
|
+
current_time = datetime.now()
|
|
140
|
+
|
|
141
|
+
def handle_worker_status(status_update):
|
|
142
|
+
if status_update.service_id not in self.map_of_workers:
|
|
143
|
+
self.map_of_workers[status_update.service_id] = status_update
|
|
144
|
+
self.map_of_workers[status_update.service_id].update_status(status_update)
|
|
145
|
+
|
|
146
|
+
def handle_job_status(status_update):
|
|
147
|
+
if status_update.job_id not in self.map_of_jobs:
|
|
148
|
+
self.map_of_jobs[status_update.job_id] = status_update
|
|
149
|
+
self.map_of_jobs[status_update.job_id].update_status(status_update)
|
|
150
|
+
|
|
151
|
+
def handle_command_status(status_update):
|
|
152
|
+
if status_update.command_id not in self.map_of_commands:
|
|
153
|
+
self.map_of_commands[status_update.command_id] = status_update
|
|
154
|
+
self.map_of_commands[status_update.command_id].update_status(status_update)
|
|
155
|
+
|
|
156
|
+
status_updater_map = {
|
|
157
|
+
WorkerStatus: handle_worker_status,
|
|
158
|
+
CommandStatus: handle_command_status,
|
|
159
|
+
JobStatus: handle_job_status,
|
|
160
|
+
}
|
|
161
|
+
while not self.status_queue.empty():
|
|
162
|
+
status_update = self.status_queue.get()
|
|
163
|
+
status_updater_map[type(status_update)](status_update)
|
|
164
|
+
|
|
165
|
+
for entity in (
|
|
166
|
+
list(self.map_of_workers.values())
|
|
167
|
+
+ list(self.map_of_commands.values())
|
|
168
|
+
+ list(self.map_of_jobs.values())
|
|
169
|
+
):
|
|
170
|
+
entity.check_if_outdated(current_time)
|
|
171
|
+
|
|
172
|
+
def pruner(entities_dictionary):
|
|
173
|
+
for key in list(entities_dictionary.keys()):
|
|
174
|
+
if (
|
|
175
|
+
entities_dictionary[key].last_update + DEAD_ENTITY_TIME_LIMIT
|
|
176
|
+
< current_time
|
|
177
|
+
):
|
|
178
|
+
del entities_dictionary[key]
|
|
179
|
+
|
|
180
|
+
pruner(self.map_of_commands)
|
|
181
|
+
pruner(self.map_of_workers)
|
|
182
|
+
pruner(self.map_of_jobs)
|
|
183
|
+
|
|
184
|
+
def list_workers(self) -> List[WorkerStatus]:
|
|
185
|
+
"""
|
|
186
|
+
:return: A list of the (known) workers with state and status information.
|
|
187
|
+
"""
|
|
188
|
+
self.update_workers()
|
|
189
|
+
return list(self.map_of_workers.values())
|
|
190
|
+
|
|
191
|
+
def list_jobs(self) -> List[JobStatus]:
|
|
192
|
+
"""
|
|
193
|
+
:return: A list of the (known) jobs with state and status information.
|
|
194
|
+
"""
|
|
195
|
+
self.update_workers()
|
|
196
|
+
return list(self.map_of_jobs.values())
|
|
197
|
+
|
|
198
|
+
def list_commands(self) -> List[CommandStatus]:
|
|
199
|
+
"""
|
|
200
|
+
:return: A list of the (known) commands and their outcomes.
|
|
201
|
+
"""
|
|
202
|
+
self.update_workers()
|
|
203
|
+
return list(self.map_of_commands.values())
|
|
204
|
+
|
|
205
|
+
def get_job(self, job_id: str) -> Union[JobStatus, None]:
|
|
206
|
+
"""
|
|
207
|
+
Get the status of a single job.
|
|
208
|
+
:param job_id: The job identifier of the job we are interested in.
|
|
209
|
+
:return: The job status or None if the job is not known.
|
|
210
|
+
"""
|
|
211
|
+
self.update_workers()
|
|
212
|
+
if job_id in self.map_of_jobs:
|
|
213
|
+
return self.map_of_jobs[job_id]
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
def get_worker(self, service_id: str) -> Union[WorkerStatus, None]:
|
|
217
|
+
"""
|
|
218
|
+
Get the status of a single worker.
|
|
219
|
+
:param service_id: The service identifier of the worker we are interested in.
|
|
220
|
+
:return: The worker status or None if the service id is not known.
|
|
221
|
+
"""
|
|
222
|
+
self.update_workers()
|
|
223
|
+
if service_id in self.map_of_workers:
|
|
224
|
+
return self.map_of_workers[service_id]
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def get_command(self, command_id: str) -> Union[CommandStatus, None]:
|
|
228
|
+
"""
|
|
229
|
+
Get the status of a single command.
|
|
230
|
+
:param command_id: The command identifier of the command we are interested in.
|
|
231
|
+
:return: The command status/outcome or None if the command is not known.
|
|
232
|
+
"""
|
|
233
|
+
self.update_workers()
|
|
234
|
+
if command_id in self.map_of_commands:
|
|
235
|
+
return self.map_of_commands[command_id]
|
|
236
|
+
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from file_writer_control.CommandChannel import CommandChannel
|
|
4
|
+
from file_writer_control.CommandStatus import CommandState
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommandHandler:
|
|
8
|
+
"""
|
|
9
|
+
A stand in for (more easily) checking the state of a command sent to a file-writer.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, command_channel: CommandChannel, command_id: str):
|
|
13
|
+
"""
|
|
14
|
+
Constructor.
|
|
15
|
+
:param command_channel: The instance of a CommandChannel that this class uses for getting the command status from.
|
|
16
|
+
:param command_id: The (unique) command identifier.
|
|
17
|
+
"""
|
|
18
|
+
self.command_id = command_id
|
|
19
|
+
self.command_channel = command_channel
|
|
20
|
+
|
|
21
|
+
def get_state(self) -> CommandState:
|
|
22
|
+
"""
|
|
23
|
+
Get the command state.
|
|
24
|
+
:return: The state of the command if possible. CommandState.UNKNOWN if the the state can not be determined.
|
|
25
|
+
"""
|
|
26
|
+
command = self.command_channel.get_command(self.command_id)
|
|
27
|
+
if command is None:
|
|
28
|
+
return CommandState.UNKNOWN
|
|
29
|
+
return command.state
|
|
30
|
+
|
|
31
|
+
def is_done(self) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
:return: True if the command completed successfully. False otherwise.
|
|
34
|
+
"""
|
|
35
|
+
current_state = self.command_channel.get_command(self.command_id).state
|
|
36
|
+
if current_state == CommandState.ERROR:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
f'Command failed with error message "{self.get_message()}".'
|
|
39
|
+
)
|
|
40
|
+
if current_state == CommandState.TIMEOUT_RESPONSE:
|
|
41
|
+
raise RuntimeError("Timed out while trying to send command.")
|
|
42
|
+
return current_state == CommandState.SUCCESS
|
|
43
|
+
|
|
44
|
+
def get_message(self) -> str:
|
|
45
|
+
"""
|
|
46
|
+
:return: If there was an error executing the command, this member function will return the error string as
|
|
47
|
+
sent by the file-writer. Will return an empty string otherwise.
|
|
48
|
+
"""
|
|
49
|
+
command = self.command_channel.get_command(self.command_id)
|
|
50
|
+
if command is None:
|
|
51
|
+
return ""
|
|
52
|
+
return command.message
|
|
53
|
+
|
|
54
|
+
def set_timeout(self, new_timeout: timedelta):
|
|
55
|
+
self.command_channel.get_command(self.command_id).timeout = new_timeout
|
|
56
|
+
|
|
57
|
+
def get_timeout(self):
|
|
58
|
+
return self.command_channel.get_command(self.command_id).timeout
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
COMMAND_STATUS_TIMEOUT = timedelta(seconds=60)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommandState(Enum):
|
|
9
|
+
"""
|
|
10
|
+
The state of a command.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
UNKNOWN = auto()
|
|
14
|
+
NO_COMMAND = auto()
|
|
15
|
+
WAITING_RESPONSE = auto()
|
|
16
|
+
TIMEOUT_RESPONSE = auto()
|
|
17
|
+
ERROR = auto()
|
|
18
|
+
SUCCESS = auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommandStatus(object):
|
|
22
|
+
"""
|
|
23
|
+
The status of a command.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
job_id: str,
|
|
29
|
+
command_id: str,
|
|
30
|
+
command_timeout: timedelta = COMMAND_STATUS_TIMEOUT,
|
|
31
|
+
):
|
|
32
|
+
self._command_timeout = command_timeout
|
|
33
|
+
self._job_id = job_id
|
|
34
|
+
self._command_id = command_id
|
|
35
|
+
self._last_update = datetime.now()
|
|
36
|
+
self._state = CommandState.NO_COMMAND
|
|
37
|
+
self._message = ""
|
|
38
|
+
self._response_code = None
|
|
39
|
+
|
|
40
|
+
def __eq__(self, other_status: "CommandStatus"):
|
|
41
|
+
if not isinstance(other_status, CommandStatus):
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
return (
|
|
44
|
+
other_status.command_id == self.command_id
|
|
45
|
+
and other_status.job_id == self.job_id
|
|
46
|
+
and other_status.state == self.state
|
|
47
|
+
and other_status.response_code == self.response_code
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def update_status(self, new_status: "CommandStatus"):
|
|
51
|
+
"""
|
|
52
|
+
Updates the status/state of this instance of the CommandStatus class using another instance.
|
|
53
|
+
.. note:: The command identifier of both this instance and the other one must be identical.
|
|
54
|
+
:param new_status: The other instance of the CommandStatus class.
|
|
55
|
+
"""
|
|
56
|
+
if new_status.command_id != self.command_id:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"Command id of status update is not correct ({self.command_id} vs {new_status.command_id})"
|
|
59
|
+
)
|
|
60
|
+
self._state = new_status.state
|
|
61
|
+
self._response_code = new_status.response_code
|
|
62
|
+
if new_status.message:
|
|
63
|
+
self._message = new_status.message
|
|
64
|
+
self._last_update = new_status.last_update
|
|
65
|
+
|
|
66
|
+
def check_if_outdated(self, current_time: datetime):
|
|
67
|
+
"""
|
|
68
|
+
Given the current time, state and the time of the last update: Have we lost the connection?
|
|
69
|
+
:param current_time: The current time
|
|
70
|
+
"""
|
|
71
|
+
if (
|
|
72
|
+
self.state != CommandState.SUCCESS
|
|
73
|
+
and self.state != CommandState.ERROR
|
|
74
|
+
and self.state != CommandState.TIMEOUT_RESPONSE
|
|
75
|
+
and current_time - self.last_update > self._command_timeout
|
|
76
|
+
):
|
|
77
|
+
self._state = CommandState.TIMEOUT_RESPONSE
|
|
78
|
+
self._last_update = current_time
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def response_code(self) -> Optional[int]:
|
|
82
|
+
"""
|
|
83
|
+
A code that mirrors the result of a command if a response has been received. Is set to None otherwise.
|
|
84
|
+
"""
|
|
85
|
+
return self._response_code
|
|
86
|
+
|
|
87
|
+
@response_code.setter
|
|
88
|
+
def response_code(self, new_code: int):
|
|
89
|
+
"""
|
|
90
|
+
Set the current response code.
|
|
91
|
+
"""
|
|
92
|
+
self._response_code = new_code
|
|
93
|
+
self._last_update = datetime.now()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def job_id(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
The job identifier under which this command is executed.
|
|
99
|
+
"""
|
|
100
|
+
return self._job_id
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def command_id(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
The unique command identifier of this command.
|
|
106
|
+
"""
|
|
107
|
+
return self._command_id
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def message(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
A status/error message as returned by the file-writer that responded to the command.
|
|
113
|
+
:return:
|
|
114
|
+
"""
|
|
115
|
+
return self._message
|
|
116
|
+
|
|
117
|
+
@message.setter
|
|
118
|
+
def message(self, new_message: str):
|
|
119
|
+
if new_message:
|
|
120
|
+
self._message = new_message
|
|
121
|
+
self._last_update = datetime.now()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def state(self) -> CommandState:
|
|
125
|
+
"""
|
|
126
|
+
The current state of the command.
|
|
127
|
+
"""
|
|
128
|
+
return self._state
|
|
129
|
+
|
|
130
|
+
@state.setter
|
|
131
|
+
def state(self, new_state: CommandState):
|
|
132
|
+
self._state = new_state
|
|
133
|
+
self._last_update = datetime.now()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def last_update(self) -> datetime:
|
|
137
|
+
"""
|
|
138
|
+
The local time stamp of the last update of the status of the command.
|
|
139
|
+
"""
|
|
140
|
+
return self._last_update
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def timeout(self) -> timedelta:
|
|
144
|
+
"""
|
|
145
|
+
Timeout for waiting for response to command.
|
|
146
|
+
"""
|
|
147
|
+
return self._command_timeout
|
|
148
|
+
|
|
149
|
+
@timeout.setter
|
|
150
|
+
def timeout(self, new_timeout: timedelta):
|
|
151
|
+
self._command_timeout = new_timeout
|