mccode-plumber 0.6.0__py3-none-any.whl → 0.7.1__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.
- mccode_plumber/epics.py +25 -11
- mccode_plumber/file_writer_control/CommandChannel.py +236 -0
- mccode_plumber/file_writer_control/CommandHandler.py +58 -0
- mccode_plumber/file_writer_control/CommandStatus.py +151 -0
- mccode_plumber/file_writer_control/InThreadStatusTracker.py +228 -0
- mccode_plumber/file_writer_control/JobHandler.py +102 -0
- mccode_plumber/file_writer_control/JobStatus.py +147 -0
- mccode_plumber/file_writer_control/KafkaTopicUrl.py +22 -0
- mccode_plumber/file_writer_control/StateExtractor.py +58 -0
- mccode_plumber/file_writer_control/WorkerFinder.py +139 -0
- mccode_plumber/file_writer_control/WorkerJobPool.py +70 -0
- mccode_plumber/file_writer_control/WorkerStatus.py +88 -0
- mccode_plumber/file_writer_control/WriteJob.py +83 -0
- mccode_plumber/file_writer_control/__init__.py +13 -0
- mccode_plumber/writer.py +3 -3
- {mccode_plumber-0.6.0.dist-info → mccode_plumber-0.7.1.dist-info}/METADATA +3 -2
- mccode_plumber-0.7.1.dist-info/RECORD +27 -0
- mccode_plumber-0.6.0.dist-info/RECORD +0 -14
- {mccode_plumber-0.6.0.dist-info → mccode_plumber-0.7.1.dist-info}/WHEEL +0 -0
- {mccode_plumber-0.6.0.dist-info → mccode_plumber-0.7.1.dist-info}/entry_points.txt +0 -0
- {mccode_plumber-0.6.0.dist-info → mccode_plumber-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from queue import Queue
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from streaming_data_types import deserialise_6s4t as deserialise_stop_time
|
|
7
|
+
from streaming_data_types import deserialise_answ as deserialise_answer
|
|
8
|
+
from streaming_data_types import deserialise_pl72 as deserialise_start
|
|
9
|
+
from streaming_data_types import deserialise_wrdn as deserialise_stopped
|
|
10
|
+
from streaming_data_types import deserialise_x5f2 as deserialise_status
|
|
11
|
+
from streaming_data_types.action_response_answ import FILE_IDENTIFIER as ANSW_IDENTIFIER
|
|
12
|
+
from streaming_data_types.action_response_answ import Response
|
|
13
|
+
from streaming_data_types.finished_writing_wrdn import (
|
|
14
|
+
FILE_IDENTIFIER as STOPPED_IDENTIFIER,
|
|
15
|
+
)
|
|
16
|
+
from streaming_data_types.finished_writing_wrdn import WritingFinished
|
|
17
|
+
from streaming_data_types.run_start_pl72 import FILE_IDENTIFIER as START_IDENTIFIER
|
|
18
|
+
from streaming_data_types.run_start_pl72 import RunStartInfo
|
|
19
|
+
from streaming_data_types.run_stop_6s4t import FILE_IDENTIFIER as STOP_TIME_IDENTIFIER
|
|
20
|
+
from streaming_data_types.run_stop_6s4t import RunStopInfo
|
|
21
|
+
from streaming_data_types.status_x5f2 import FILE_IDENTIFIER as STAT_IDENTIFIER
|
|
22
|
+
from streaming_data_types.status_x5f2 import StatusMessage
|
|
23
|
+
from streaming_data_types.utils import get_schema
|
|
24
|
+
|
|
25
|
+
from file_writer_control.CommandStatus import CommandState, CommandStatus
|
|
26
|
+
from file_writer_control.JobStatus import JobState, JobStatus
|
|
27
|
+
from file_writer_control.StateExtractor import (
|
|
28
|
+
extract_job_state_from_answer,
|
|
29
|
+
extract_state_from_command_answer,
|
|
30
|
+
extract_worker_state_from_status,
|
|
31
|
+
)
|
|
32
|
+
from file_writer_control.WorkerStatus import WorkerState, WorkerStatus
|
|
33
|
+
|
|
34
|
+
DEAD_ENTITY_TIME_LIMIT = timedelta(hours=1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InThreadStatusTracker:
|
|
38
|
+
"""
|
|
39
|
+
Implements de-coding of flatbuffer messages and sends updates of worker, job and command state/status back to the
|
|
40
|
+
"main"-thread if there has been changes.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, status_queue: Queue):
|
|
44
|
+
"""
|
|
45
|
+
Constructor.
|
|
46
|
+
:param status_queue: The output queue to which state/status updates are pushed.
|
|
47
|
+
"""
|
|
48
|
+
self.queue = status_queue
|
|
49
|
+
self.known_workers: Dict[str, WorkerStatus] = {}
|
|
50
|
+
self.known_jobs: Dict[str, JobStatus] = {}
|
|
51
|
+
self.known_commands: Dict[str, CommandStatus] = {}
|
|
52
|
+
|
|
53
|
+
def process_message(self, message: bytes):
|
|
54
|
+
"""
|
|
55
|
+
Process a binary message.
|
|
56
|
+
:param message: The binary message to be processed.
|
|
57
|
+
"""
|
|
58
|
+
current_schema = get_schema(message).encode("utf-8")
|
|
59
|
+
update_time = datetime.now()
|
|
60
|
+
msg_process_map = {
|
|
61
|
+
ANSW_IDENTIFIER: lambda msg: self.process_answer(deserialise_answer(msg)),
|
|
62
|
+
STAT_IDENTIFIER: lambda msg: self.process_status(deserialise_status(msg)),
|
|
63
|
+
STOP_TIME_IDENTIFIER: lambda msg: self.process_set_stop_time(
|
|
64
|
+
deserialise_stop_time(msg)
|
|
65
|
+
),
|
|
66
|
+
START_IDENTIFIER: lambda msg: self.process_start(deserialise_start(msg)),
|
|
67
|
+
STOPPED_IDENTIFIER: lambda msg: self.process_stopped(
|
|
68
|
+
deserialise_stopped(msg)
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
if current_schema in msg_process_map:
|
|
72
|
+
msg_process_map[current_schema](message)
|
|
73
|
+
|
|
74
|
+
self.send_status_if_updated(update_time)
|
|
75
|
+
|
|
76
|
+
def send_status_if_updated(self, limit_time: datetime):
|
|
77
|
+
"""
|
|
78
|
+
Sends status updates of workers, jobs and commands (to the status queue) if there has been any updates on or
|
|
79
|
+
after the limit_time.
|
|
80
|
+
:param limit_time: The cut-off time for deciding which updates should be sent to the status queue.
|
|
81
|
+
"""
|
|
82
|
+
for entity in (
|
|
83
|
+
list(self.known_workers.values())
|
|
84
|
+
+ list(self.known_jobs.values())
|
|
85
|
+
+ list(self.known_commands.values())
|
|
86
|
+
):
|
|
87
|
+
if entity.last_update >= limit_time:
|
|
88
|
+
self.queue.put(entity)
|
|
89
|
+
|
|
90
|
+
def check_for_worker_presence(self, service_id: str):
|
|
91
|
+
"""
|
|
92
|
+
Check if a service_id is known and add it to a list of known ones if it is not.
|
|
93
|
+
:param service_id: The service identifier to look for.
|
|
94
|
+
"""
|
|
95
|
+
if service_id not in self.known_workers:
|
|
96
|
+
self.known_workers[service_id] = WorkerStatus(service_id)
|
|
97
|
+
|
|
98
|
+
def check_for_job_presence(self, job_id: str):
|
|
99
|
+
"""
|
|
100
|
+
Check if a job identifier is known and add it to a list of known ones if it is not.
|
|
101
|
+
:param job_id: The job identifier to look for.
|
|
102
|
+
"""
|
|
103
|
+
if job_id not in self.known_jobs:
|
|
104
|
+
new_job = JobStatus(job_id)
|
|
105
|
+
self.known_jobs[job_id] = new_job
|
|
106
|
+
|
|
107
|
+
def check_for_command_presence(self, job_id: str, command_id: str):
|
|
108
|
+
"""
|
|
109
|
+
Check if a command identifier is known and add it to a list of known ones if it is not.
|
|
110
|
+
:param job_id: The job identifier of the command that we are looking for. (Only used if we need to add the
|
|
111
|
+
command.)
|
|
112
|
+
:param command_id: The command identifier to look for.
|
|
113
|
+
"""
|
|
114
|
+
if command_id not in self.known_commands:
|
|
115
|
+
new_command = CommandStatus(job_id, command_id)
|
|
116
|
+
self.known_commands[command_id] = new_command
|
|
117
|
+
|
|
118
|
+
def check_for_lost_connections(self):
|
|
119
|
+
"""
|
|
120
|
+
Check workers, commands and jobs for the last update time and change the state of these if a timeout has been
|
|
121
|
+
reached.
|
|
122
|
+
"""
|
|
123
|
+
now = datetime.now()
|
|
124
|
+
for entity in (
|
|
125
|
+
list(self.known_workers.values())
|
|
126
|
+
+ list(self.known_jobs.values())
|
|
127
|
+
+ list(self.known_commands.values())
|
|
128
|
+
):
|
|
129
|
+
entity.check_if_outdated(now)
|
|
130
|
+
self.send_status_if_updated(now)
|
|
131
|
+
|
|
132
|
+
def prune_dead_entities(self, current_time: datetime):
|
|
133
|
+
"""
|
|
134
|
+
Will remove old jobs, workers and commands that have not been updated recently.
|
|
135
|
+
:return:
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def pruner(entities_dictionary):
|
|
139
|
+
for key in list(entities_dictionary.keys()):
|
|
140
|
+
if (
|
|
141
|
+
entities_dictionary[key].last_update + DEAD_ENTITY_TIME_LIMIT
|
|
142
|
+
< current_time
|
|
143
|
+
):
|
|
144
|
+
del entities_dictionary[key]
|
|
145
|
+
|
|
146
|
+
pruner(self.known_workers)
|
|
147
|
+
pruner(self.known_commands)
|
|
148
|
+
pruner(self.known_jobs)
|
|
149
|
+
|
|
150
|
+
def process_answer(self, answer: Response):
|
|
151
|
+
"""
|
|
152
|
+
Update workers, jobs and commands based on information in a response message.
|
|
153
|
+
:param answer: The response/answer message to use for status updates.
|
|
154
|
+
"""
|
|
155
|
+
self.check_for_worker_presence(answer.service_id)
|
|
156
|
+
self.check_for_job_presence(answer.job_id)
|
|
157
|
+
self.check_for_command_presence(answer.job_id, answer.command_id)
|
|
158
|
+
new_job_state = extract_job_state_from_answer(answer)
|
|
159
|
+
if new_job_state is not None:
|
|
160
|
+
self.known_jobs[answer.job_id].state = new_job_state
|
|
161
|
+
current_command = self.known_commands[answer.command_id]
|
|
162
|
+
current_command.state = extract_state_from_command_answer(answer)
|
|
163
|
+
current_command.message = answer.message
|
|
164
|
+
current_command.response_code = Response.status_code
|
|
165
|
+
self.known_jobs[answer.job_id].message = answer.message
|
|
166
|
+
try:
|
|
167
|
+
self.known_jobs[answer.job_id].service_id = answer.service_id
|
|
168
|
+
except RuntimeError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def process_status(self, status_update: StatusMessage):
|
|
172
|
+
"""
|
|
173
|
+
Update workers and jobs based on information in a status message.
|
|
174
|
+
:param status_update: The status message to use for updates.
|
|
175
|
+
"""
|
|
176
|
+
self.check_for_worker_presence(status_update.service_id)
|
|
177
|
+
current_state = extract_worker_state_from_status(status_update)
|
|
178
|
+
self.known_workers[status_update.service_id].state = current_state
|
|
179
|
+
if current_state == WorkerState.WRITING:
|
|
180
|
+
try:
|
|
181
|
+
json_data = json.loads(status_update.status_json)
|
|
182
|
+
job_id = json_data["job_id"]
|
|
183
|
+
except (KeyError, json.JSONDecodeError):
|
|
184
|
+
raise RuntimeError("Unable to extract JSON data from status message.")
|
|
185
|
+
file_name = json_data["file_being_written"]
|
|
186
|
+
self.check_for_job_presence(job_id)
|
|
187
|
+
self.known_jobs[job_id].state = JobState.WRITING
|
|
188
|
+
self.known_jobs[job_id].file_name = file_name
|
|
189
|
+
self.known_jobs[job_id].metadata = json_data
|
|
190
|
+
# For some jobs, we will only know the service-id when a worker starts working on a job.
|
|
191
|
+
# Thus we need the following statement to update the (known) service-id of a job.
|
|
192
|
+
try:
|
|
193
|
+
self.known_jobs[job_id].service_id = status_update.service_id
|
|
194
|
+
except RuntimeError:
|
|
195
|
+
pass # Expected error (i.e. the job is not known), do nothing
|
|
196
|
+
|
|
197
|
+
def process_set_stop_time(self, stop_time: RunStopInfo):
|
|
198
|
+
"""
|
|
199
|
+
Update commands and jobs based on information in a "set stop time" message.
|
|
200
|
+
:param stop_time: The "stop" message to use for updates.
|
|
201
|
+
"""
|
|
202
|
+
self.check_for_command_presence(stop_time.job_id, stop_time.command_id)
|
|
203
|
+
self.known_commands[stop_time.command_id].state = CommandState.WAITING_RESPONSE
|
|
204
|
+
|
|
205
|
+
def process_start(self, start: RunStartInfo):
|
|
206
|
+
"""
|
|
207
|
+
Update commands and jobs based on information in a "start" message.
|
|
208
|
+
:param start: The "start" message to use for updates.
|
|
209
|
+
"""
|
|
210
|
+
self.check_for_job_presence(start.job_id)
|
|
211
|
+
self.check_for_command_presence(start.job_id, start.job_id)
|
|
212
|
+
self.known_commands[start.job_id].state = CommandState.WAITING_RESPONSE
|
|
213
|
+
|
|
214
|
+
def process_stopped(self, stopped: WritingFinished):
|
|
215
|
+
"""
|
|
216
|
+
Update workers and jobs based on information in a "has stopped" message.
|
|
217
|
+
:param stopped: The "stopped" message to use for updates.
|
|
218
|
+
"""
|
|
219
|
+
self.check_for_job_presence(stopped.job_id)
|
|
220
|
+
self.check_for_worker_presence(stopped.service_id)
|
|
221
|
+
current_job = self.known_jobs[stopped.job_id]
|
|
222
|
+
if stopped.error_encountered:
|
|
223
|
+
current_job.state = JobState.ERROR
|
|
224
|
+
else:
|
|
225
|
+
current_job.state = JobState.DONE
|
|
226
|
+
current_job.metadata = json.loads(stopped.metadata)
|
|
227
|
+
current_job.message = stopped.message
|
|
228
|
+
self.known_workers[stopped.service_id].state = WorkerState.IDLE
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from file_writer_control.CommandHandler import CommandHandler
|
|
4
|
+
from file_writer_control.JobStatus import JobState
|
|
5
|
+
from file_writer_control.WorkerFinder import WorkerFinder
|
|
6
|
+
from file_writer_control.WriteJob import WriteJob
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JobHandler:
|
|
10
|
+
"""
|
|
11
|
+
A stand in for controlling and checking the state of a job running on a file-writer instance.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, worker_finder: WorkerFinder, job_id=""):
|
|
15
|
+
"""
|
|
16
|
+
Constructor.
|
|
17
|
+
:param worker_finder: An instance of a class that inherits from WorkerFinder and implements the member function
|
|
18
|
+
try_start_job of that class.
|
|
19
|
+
:param job_id: (Optional) The job identifier of an existing job.
|
|
20
|
+
"""
|
|
21
|
+
self.worker_finder = worker_finder
|
|
22
|
+
self._job_id = job_id
|
|
23
|
+
|
|
24
|
+
def start_job(self, job: WriteJob) -> CommandHandler:
|
|
25
|
+
"""
|
|
26
|
+
Start a write job. This call is not blocking. It does not guarantee that the write job will actually be started.
|
|
27
|
+
:param job: The write to be started.
|
|
28
|
+
.. note:: Starting a new job will cause the current instance of this class to no longer being able to track or
|
|
29
|
+
control previous jobs.
|
|
30
|
+
:return: A CommandHandler instance that can be used to monitor the outcome of the attempt to start a write job.
|
|
31
|
+
"""
|
|
32
|
+
self._job_id = job.job_id
|
|
33
|
+
return self.worker_finder.try_start_job(job)
|
|
34
|
+
|
|
35
|
+
def get_state(self) -> JobState:
|
|
36
|
+
"""
|
|
37
|
+
Get the state of the job.
|
|
38
|
+
"""
|
|
39
|
+
return self.worker_finder.get_job_state(self._job_id)
|
|
40
|
+
|
|
41
|
+
def is_done(self) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
:return: True if job was completed without errors. False otherwise.
|
|
44
|
+
.. note:: If the job was completed with errors, this call will return False.
|
|
45
|
+
"""
|
|
46
|
+
current_job_state = self.worker_finder.get_job_state(self._job_id)
|
|
47
|
+
if current_job_state == JobState.ERROR:
|
|
48
|
+
raise RuntimeError(f'Job failed with error message "{self.get_message()}".')
|
|
49
|
+
if current_job_state == JobState.TIMEOUT:
|
|
50
|
+
raise RuntimeError("Timed out while trying to start write job.")
|
|
51
|
+
return current_job_state == JobState.DONE
|
|
52
|
+
|
|
53
|
+
def get_message(self) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Get a string describing the error that was encountered when running the job. (If there was an error.)
|
|
56
|
+
"""
|
|
57
|
+
current_status = self.worker_finder.get_job_status(self._job_id)
|
|
58
|
+
if current_status is None:
|
|
59
|
+
return ""
|
|
60
|
+
return current_status.message
|
|
61
|
+
|
|
62
|
+
def set_stop_time(self, stop_time: datetime) -> CommandHandler:
|
|
63
|
+
"""
|
|
64
|
+
Set a new stop time for the file-writing job. There is no guarantee that the stop time will actually be changed.
|
|
65
|
+
This call is not blocking. Calling this member function will have no effect on the stop-time before the write
|
|
66
|
+
job has started.
|
|
67
|
+
|
|
68
|
+
:param stop_time: The new stop time of the job.
|
|
69
|
+
:return: A CommandHandler instance that can be used to monitor the outcome of the attempt to set a new stop time.
|
|
70
|
+
"""
|
|
71
|
+
current_status = self.worker_finder.get_job_status(self._job_id)
|
|
72
|
+
return self.worker_finder.try_send_stop_time(
|
|
73
|
+
current_status.service_id if current_status else None,
|
|
74
|
+
self._job_id,
|
|
75
|
+
stop_time,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def stop_now(self) -> CommandHandler:
|
|
79
|
+
"""
|
|
80
|
+
See the documentation for abort_write_job().
|
|
81
|
+
"""
|
|
82
|
+
return self.abort_write_job()
|
|
83
|
+
|
|
84
|
+
def abort_write_job(self) -> CommandHandler:
|
|
85
|
+
"""
|
|
86
|
+
Tell the file-writing to abort writing. There is no guarantee that will actually happen though.
|
|
87
|
+
This call is not blocking. Calling this member function will have no effect if done before a write job has
|
|
88
|
+
actually started.
|
|
89
|
+
|
|
90
|
+
:return: A CommandHandler instance that can be used to monitor the outcome of the attempt to set a new stop time.
|
|
91
|
+
"""
|
|
92
|
+
current_status = self.worker_finder.get_job_status(self._job_id)
|
|
93
|
+
return self.worker_finder.try_send_abort(
|
|
94
|
+
current_status.service_id if current_status else None, self._job_id
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def job_id(self) -> str:
|
|
99
|
+
"""
|
|
100
|
+
The job identifier of the job that this instance of the JobHandler class represent.
|
|
101
|
+
"""
|
|
102
|
+
return self._job_id
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
DEFAULT_TIMEOUT = timedelta(seconds=15)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JobState(Enum):
|
|
9
|
+
"""
|
|
10
|
+
The state of a job.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
NO_JOB = auto()
|
|
14
|
+
WAITING = auto()
|
|
15
|
+
WRITING = auto()
|
|
16
|
+
TIMEOUT = auto()
|
|
17
|
+
ERROR = auto()
|
|
18
|
+
DONE = auto()
|
|
19
|
+
UNAVAILABLE = auto()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JobStatus:
|
|
23
|
+
"""
|
|
24
|
+
Contains general information about the (execution) of a job.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, job_id: str, timeout: Optional[timedelta] = DEFAULT_TIMEOUT):
|
|
28
|
+
self._job_id = job_id
|
|
29
|
+
self._timeout = timeout
|
|
30
|
+
self._service_id = ""
|
|
31
|
+
self._file_name = ""
|
|
32
|
+
self._last_update = datetime.now()
|
|
33
|
+
self._state = JobState.WAITING
|
|
34
|
+
self._metadata: Optional[Dict] = None
|
|
35
|
+
self._message = ""
|
|
36
|
+
|
|
37
|
+
def update_status(self, new_status: "JobStatus") -> None:
|
|
38
|
+
"""
|
|
39
|
+
Updates the status/state of a this instance of the JobStatus class, using another instance.
|
|
40
|
+
.. note:: The job identifier of this instance and the other must be identical.
|
|
41
|
+
:param new_status: The other instance of the JobStatus class.
|
|
42
|
+
"""
|
|
43
|
+
if new_status.job_id != self.job_id:
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"Job id of status update is not correct ({self.job_id} vs {new_status.job_id})"
|
|
46
|
+
)
|
|
47
|
+
self._state = new_status.state
|
|
48
|
+
if new_status.message:
|
|
49
|
+
self._message = new_status.message
|
|
50
|
+
self._service_id = new_status.service_id
|
|
51
|
+
self._file_name = new_status.file_name
|
|
52
|
+
self._last_update = new_status.last_update
|
|
53
|
+
self._metadata = new_status.metadata
|
|
54
|
+
|
|
55
|
+
def check_if_outdated(self, current_time: datetime):
|
|
56
|
+
"""
|
|
57
|
+
Given the current time, state and the time of the last update: Have we lost the connection?
|
|
58
|
+
:param current_time: The current time
|
|
59
|
+
"""
|
|
60
|
+
if (
|
|
61
|
+
self.state != JobState.DONE
|
|
62
|
+
and self.state != JobState.ERROR
|
|
63
|
+
and self.state != JobState.TIMEOUT
|
|
64
|
+
and current_time - self.last_update > self._timeout
|
|
65
|
+
):
|
|
66
|
+
self._state = JobState.TIMEOUT
|
|
67
|
+
self._last_update = current_time
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def job_id(self) -> str:
|
|
71
|
+
"""
|
|
72
|
+
The (unique) job identifier.
|
|
73
|
+
"""
|
|
74
|
+
return self._job_id
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def service_id(self) -> str:
|
|
78
|
+
"""
|
|
79
|
+
The (unique) service identifier of the instance of the file-writer that executes the current job.
|
|
80
|
+
"""
|
|
81
|
+
return self._service_id
|
|
82
|
+
|
|
83
|
+
@service_id.setter
|
|
84
|
+
def service_id(self, new_service_id: str) -> None:
|
|
85
|
+
if not self._service_id:
|
|
86
|
+
self._service_id = new_service_id
|
|
87
|
+
self._last_update = datetime.now()
|
|
88
|
+
elif self._service_id == new_service_id:
|
|
89
|
+
return
|
|
90
|
+
else:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f'Can not set service_id of job with id "{self._job_id}" to "{new_service_id}" as it has already been set to "{self._service_id}".'
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def last_update(self) -> datetime:
|
|
97
|
+
"""
|
|
98
|
+
The local time stamp of the last update of the status of the job.
|
|
99
|
+
"""
|
|
100
|
+
return self._last_update
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def state(self) -> JobState:
|
|
104
|
+
"""
|
|
105
|
+
The current state of the job.
|
|
106
|
+
"""
|
|
107
|
+
return self._state
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def file_name(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
The file name of the job. None if the file name is not known.
|
|
113
|
+
"""
|
|
114
|
+
if self._file_name == "":
|
|
115
|
+
return None
|
|
116
|
+
return self._file_name
|
|
117
|
+
|
|
118
|
+
@file_name.setter
|
|
119
|
+
def file_name(self, new_file_name: str) -> None:
|
|
120
|
+
self._file_name = new_file_name
|
|
121
|
+
self._last_update = datetime.now()
|
|
122
|
+
|
|
123
|
+
@state.setter
|
|
124
|
+
def state(self, new_state: JobState) -> None:
|
|
125
|
+
self._state = new_state
|
|
126
|
+
self._last_update = datetime.now()
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def message(self) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Status/state message of the job as received from the file-writer.
|
|
132
|
+
"""
|
|
133
|
+
return self._message
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def metadata(self) -> Optional[Dict]:
|
|
137
|
+
return self._metadata
|
|
138
|
+
|
|
139
|
+
@metadata.setter
|
|
140
|
+
def metadata(self, metadata: Dict) -> None:
|
|
141
|
+
self._metadata = metadata
|
|
142
|
+
|
|
143
|
+
@message.setter
|
|
144
|
+
def message(self, new_message: str) -> None:
|
|
145
|
+
if new_message:
|
|
146
|
+
self._message = new_message
|
|
147
|
+
self._last_update = datetime.now()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KafkaTopicUrl:
|
|
5
|
+
"""
|
|
6
|
+
Class for extracting address, port and topic name from a Kafka topic url.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
test_regexp = re.compile(
|
|
10
|
+
r"^\s*(?:kafka://)?(?:(?P<host>[^/?#:]+)(?::(?P<port>\d+){1,5})?)/(?P<topic>[a-zA-Z0-9._-]+)\s*$"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def __init__(self, url: str):
|
|
14
|
+
result = re.match(KafkaTopicUrl.test_regexp, url)
|
|
15
|
+
if result is None:
|
|
16
|
+
raise RuntimeError("Unable to match kafka url.")
|
|
17
|
+
self.port = 9092 # Default Kafka broker port
|
|
18
|
+
if result.group("port") is not None:
|
|
19
|
+
self.port = int(result.group("port"))
|
|
20
|
+
self.host = result.group("host")
|
|
21
|
+
self.host_port = f"{self.host}:{self.port}"
|
|
22
|
+
self.topic = result.group("topic")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from json import loads
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from streaming_data_types.action_response_answ import (
|
|
5
|
+
ActionOutcome,
|
|
6
|
+
ActionResponse,
|
|
7
|
+
ActionType,
|
|
8
|
+
)
|
|
9
|
+
from streaming_data_types.status_x5f2 import StatusMessage
|
|
10
|
+
|
|
11
|
+
from file_writer_control.CommandStatus import CommandState
|
|
12
|
+
from file_writer_control.JobStatus import JobState
|
|
13
|
+
from file_writer_control.WorkerStatus import WorkerState
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_worker_state_from_status(status: StatusMessage) -> WorkerState:
|
|
17
|
+
"""
|
|
18
|
+
Determine the worker state (i.e. file-writer state) based on a file-writer status message.
|
|
19
|
+
:param status: A status update message from a file-writer.
|
|
20
|
+
:return: The extracted worker state.
|
|
21
|
+
"""
|
|
22
|
+
json_struct = loads(status.status_json)
|
|
23
|
+
status_map = {"writing": WorkerState.WRITING, "idle": WorkerState.IDLE}
|
|
24
|
+
try:
|
|
25
|
+
status_string = json_struct["state"]
|
|
26
|
+
return status_map[status_string]
|
|
27
|
+
except KeyError:
|
|
28
|
+
return WorkerState.UNKNOWN
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_state_from_command_answer(answer: ActionResponse) -> CommandState:
|
|
32
|
+
"""
|
|
33
|
+
Determine the command state from a action response message.
|
|
34
|
+
:param answer: The action (either "start a job" or "set top time") response from a file-writer.
|
|
35
|
+
:return: The extracted command state/response.
|
|
36
|
+
"""
|
|
37
|
+
status_map = {
|
|
38
|
+
ActionOutcome.Failure: CommandState.ERROR,
|
|
39
|
+
ActionOutcome.Success: CommandState.SUCCESS,
|
|
40
|
+
}
|
|
41
|
+
try:
|
|
42
|
+
return status_map[answer.outcome]
|
|
43
|
+
except KeyError:
|
|
44
|
+
return CommandState.ERROR
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def extract_job_state_from_answer(answer: ActionResponse) -> Optional[JobState]:
|
|
48
|
+
"""
|
|
49
|
+
Determine the file writing job state from a action response message.
|
|
50
|
+
:param answer: The action (either "start a job" or "set top time") response from a file-writer.
|
|
51
|
+
:return: The extracted job state, None if job state can not be determined from this answer.
|
|
52
|
+
"""
|
|
53
|
+
if answer.action == ActionType.StartJob:
|
|
54
|
+
if answer.outcome == ActionOutcome.Success:
|
|
55
|
+
return JobState.WRITING
|
|
56
|
+
else:
|
|
57
|
+
return JobState.ERROR
|
|
58
|
+
return None
|