meerschaum 2.0.0rc7__py3-none-any.whl → 2.0.0rc9__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.
- meerschaum/actions/__init__.py +97 -48
- meerschaum/actions/bootstrap.py +1 -1
- meerschaum/actions/clear.py +1 -1
- meerschaum/actions/deduplicate.py +1 -1
- meerschaum/actions/delete.py +8 -7
- meerschaum/actions/drop.py +1 -10
- meerschaum/actions/edit.py +1 -1
- meerschaum/actions/install.py +1 -1
- meerschaum/actions/pause.py +1 -1
- meerschaum/actions/register.py +1 -1
- meerschaum/actions/setup.py +1 -1
- meerschaum/actions/show.py +1 -1
- meerschaum/actions/start.py +18 -7
- meerschaum/actions/stop.py +5 -4
- meerschaum/actions/sync.py +3 -1
- meerschaum/actions/uninstall.py +1 -1
- meerschaum/actions/upgrade.py +1 -1
- meerschaum/actions/verify.py +54 -3
- meerschaum/config/_default.py +1 -1
- meerschaum/config/_formatting.py +26 -0
- meerschaum/config/_jobs.py +28 -5
- meerschaum/config/_paths.py +21 -5
- meerschaum/config/_version.py +1 -1
- meerschaum/connectors/api/_fetch.py +40 -38
- meerschaum/connectors/api/_pipes.py +10 -17
- meerschaum/connectors/sql/_fetch.py +29 -11
- meerschaum/connectors/sql/_pipes.py +1 -2
- meerschaum/core/Pipe/__init__.py +31 -10
- meerschaum/core/Pipe/_data.py +23 -13
- meerschaum/core/Pipe/_deduplicate.py +44 -23
- meerschaum/core/Pipe/_dtypes.py +2 -1
- meerschaum/core/Pipe/_fetch.py +29 -0
- meerschaum/core/Pipe/_sync.py +25 -18
- meerschaum/core/Pipe/_verify.py +60 -25
- meerschaum/plugins/__init__.py +3 -0
- meerschaum/utils/daemon/Daemon.py +108 -27
- meerschaum/utils/daemon/__init__.py +35 -1
- meerschaum/utils/dataframe.py +2 -0
- meerschaum/utils/formatting/__init__.py +144 -1
- meerschaum/utils/formatting/_pipes.py +28 -5
- meerschaum/utils/misc.py +184 -188
- meerschaum/utils/packages/__init__.py +1 -1
- meerschaum/utils/packages/_packages.py +1 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/METADATA +4 -1
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/RECORD +51 -51
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/LICENSE +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/NOTICE +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/WHEEL +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/top_level.txt +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/zip-safe +0 -0
meerschaum/core/Pipe/_sync.py
CHANGED
@@ -14,7 +14,16 @@ import threading
|
|
14
14
|
from datetime import datetime, timedelta
|
15
15
|
|
16
16
|
from meerschaum.utils.typing import (
|
17
|
-
Union,
|
17
|
+
Union,
|
18
|
+
Optional,
|
19
|
+
Callable,
|
20
|
+
Any,
|
21
|
+
Tuple,
|
22
|
+
SuccessTuple,
|
23
|
+
Dict,
|
24
|
+
List,
|
25
|
+
Iterable,
|
26
|
+
Generator,
|
18
27
|
Iterator,
|
19
28
|
)
|
20
29
|
|
@@ -29,8 +38,8 @@ def sync(
|
|
29
38
|
List[Dict[str, Any]],
|
30
39
|
InferFetch
|
31
40
|
] = InferFetch,
|
32
|
-
begin:
|
33
|
-
end:
|
41
|
+
begin: Union[datetime, int, str, None] = '',
|
42
|
+
end: Union[datetime, int] = None,
|
34
43
|
force: bool = False,
|
35
44
|
retries: int = 10,
|
36
45
|
min_seconds: int = 1,
|
@@ -55,28 +64,23 @@ def sync(
|
|
55
64
|
df: Union[None, pd.DataFrame, Dict[str, List[Any]]], default None
|
56
65
|
An optional DataFrame to sync into the pipe. Defaults to `None`.
|
57
66
|
|
58
|
-
begin:
|
67
|
+
begin: Union[datetime, int, str, None], default ''
|
59
68
|
Optionally specify the earliest datetime to search for data.
|
60
|
-
Defaults to `None`.
|
61
69
|
|
62
|
-
end:
|
70
|
+
end: Union[datetime, int, str, None], default None
|
63
71
|
Optionally specify the latest datetime to search for data.
|
64
|
-
Defaults to `None`.
|
65
72
|
|
66
73
|
force: bool, default False
|
67
74
|
If `True`, keep trying to sync untul `retries` attempts.
|
68
|
-
Defaults to `False`.
|
69
75
|
|
70
76
|
retries: int, default 10
|
71
77
|
If `force`, how many attempts to try syncing before declaring failure.
|
72
|
-
Defaults to `10`.
|
73
78
|
|
74
79
|
min_seconds: Union[int, float], default 1
|
75
80
|
If `force`, how many seconds to sleep between retries. Defaults to `1`.
|
76
81
|
|
77
82
|
check_existing: bool, default True
|
78
83
|
If `True`, pull and diff with existing data from the pipe.
|
79
|
-
Defaults to `True`.
|
80
84
|
|
81
85
|
blocking: bool, default True
|
82
86
|
If `True`, wait for sync to finish and return its result, otherwise
|
@@ -87,7 +91,6 @@ def sync(
|
|
87
91
|
If provided and the instance connector is thread-safe
|
88
92
|
(`pipe.instance_connector.IS_THREAD_SAFE is True`),
|
89
93
|
limit concurrent sync to this many threads.
|
90
|
-
Defaults to `None`.
|
91
94
|
|
92
95
|
callback: Optional[Callable[[Tuple[bool, str]], Any]], default None
|
93
96
|
Callback function which expects a SuccessTuple as input.
|
@@ -101,7 +104,6 @@ def sync(
|
|
101
104
|
Specify the number of rows to sync per chunk.
|
102
105
|
If `-1`, resort to system configuration (default is `900`).
|
103
106
|
A `chunksize` of `None` will sync all rows in one transaction.
|
104
|
-
Defaults to `-1`.
|
105
107
|
|
106
108
|
sync_chunks: bool, default True
|
107
109
|
If possible, sync chunks while fetching them into memory.
|
@@ -112,7 +114,6 @@ def sync(
|
|
112
114
|
Returns
|
113
115
|
-------
|
114
116
|
A `SuccessTuple` of success (`bool`) and message (`str`).
|
115
|
-
|
116
117
|
"""
|
117
118
|
from meerschaum.utils.debug import dprint, _checkpoint
|
118
119
|
from meerschaum.utils.warnings import warn, error
|
@@ -183,7 +184,7 @@ def sync(
|
|
183
184
|
### use that instead.
|
184
185
|
### NOTE: The DataFrame must be omitted for the plugin sync method to apply.
|
185
186
|
### If a DataFrame is provided, continue as expected.
|
186
|
-
if hasattr(df, 'MRSM_INFER_FETCH'):
|
187
|
+
if hasattr(df, 'MRSM_INFER_FETCH'):
|
187
188
|
try:
|
188
189
|
if p.connector is None:
|
189
190
|
msg = f"{p} does not have a valid connector."
|
@@ -430,13 +431,19 @@ def sync(
|
|
430
431
|
|
431
432
|
def _determine_begin(
|
432
433
|
pipe: meerschaum.Pipe,
|
433
|
-
begin:
|
434
|
+
begin: Union[datetime, int, str] = '',
|
434
435
|
debug: bool = False,
|
435
436
|
) -> Union[datetime, int, None]:
|
436
|
-
|
437
|
-
if begin is not
|
437
|
+
"""
|
438
|
+
Apply the backtrack interval if `--begin` is not provided.
|
439
|
+
"""
|
440
|
+
if begin != '':
|
438
441
|
return begin
|
439
|
-
|
442
|
+
sync_time = pipe.get_sync_time(debug=debug)
|
443
|
+
if sync_time is None:
|
444
|
+
return sync_time
|
445
|
+
backtrack_interval = pipe.get_backtrack_interval(debug=debug)
|
446
|
+
return sync_time - backtrack_interval
|
440
447
|
|
441
448
|
|
442
449
|
def get_sync_time(
|
meerschaum/core/Pipe/_verify.py
CHANGED
@@ -62,6 +62,7 @@ def verify(
|
|
62
62
|
A SuccessTuple indicating whether the pipe was successfully resynced.
|
63
63
|
"""
|
64
64
|
from meerschaum.utils.pool import get_pool
|
65
|
+
from meerschaum.utils.misc import interval_str
|
65
66
|
workers = self.get_num_workers(workers)
|
66
67
|
|
67
68
|
### Skip configured bounding in parameters
|
@@ -74,16 +75,16 @@ def verify(
|
|
74
75
|
if bounded is None:
|
75
76
|
bounded = bound_time is not None
|
76
77
|
|
77
|
-
if begin is None:
|
78
|
+
if bounded and begin is None:
|
78
79
|
begin = (
|
79
80
|
bound_time
|
80
81
|
if bound_time is not None
|
81
82
|
else self.get_sync_time(newest=False, debug=debug)
|
82
83
|
)
|
83
|
-
if end is None:
|
84
|
+
if bounded and end is None:
|
84
85
|
end = self.get_sync_time(newest=True, debug=debug)
|
85
86
|
|
86
|
-
if bounded:
|
87
|
+
if bounded and end is not None:
|
87
88
|
end += (
|
88
89
|
timedelta(minutes=1)
|
89
90
|
if isinstance(end, datetime)
|
@@ -93,13 +94,7 @@ def verify(
|
|
93
94
|
sync_less_than_begin = not bounded and begin is None
|
94
95
|
sync_greater_than_end = not bounded and end is None
|
95
96
|
|
96
|
-
cannot_determine_bounds = (
|
97
|
-
begin is None
|
98
|
-
or
|
99
|
-
end is None
|
100
|
-
or
|
101
|
-
not self.exists(debug=debug)
|
102
|
-
)
|
97
|
+
cannot_determine_bounds = not self.exists(debug=debug)
|
103
98
|
|
104
99
|
if cannot_determine_bounds:
|
105
100
|
sync_success, sync_msg = self.sync(
|
@@ -146,21 +141,48 @@ def verify(
|
|
146
141
|
)
|
147
142
|
return True, f"Could not determine chunks between '{begin}' and '{end}'; nothing to do."
|
148
143
|
|
144
|
+
begin_to_print = (
|
145
|
+
begin
|
146
|
+
if begin is not None
|
147
|
+
else (
|
148
|
+
chunk_bounds[0][0]
|
149
|
+
if bounded
|
150
|
+
else chunk_bounds[0][1]
|
151
|
+
)
|
152
|
+
)
|
153
|
+
end_to_print = (
|
154
|
+
end
|
155
|
+
if end is not None
|
156
|
+
else (
|
157
|
+
chunk_bounds[-1][1]
|
158
|
+
if bounded
|
159
|
+
else chunk_bounds[-1][0]
|
160
|
+
)
|
161
|
+
)
|
162
|
+
|
149
163
|
info(
|
150
164
|
f"Syncing {len(chunk_bounds)} chunk" + ('s' if len(chunk_bounds) != 1 else '')
|
151
165
|
+ f" ({'un' if not bounded else ''}bounded)"
|
152
|
-
+ f" of size '{chunk_interval}'"
|
153
|
-
+ f" between '{
|
166
|
+
+ f" of size '{interval_str(chunk_interval)}'"
|
167
|
+
+ f" between '{begin_to_print}' and '{end_to_print}'."
|
154
168
|
)
|
155
169
|
|
156
170
|
pool = get_pool(workers=workers)
|
157
171
|
|
172
|
+
### Dictionary of the form bounds -> success_tuple, e.g.:
|
173
|
+
### {
|
174
|
+
### (2023-01-01, 2023-01-02): (True, "Success")
|
175
|
+
### }
|
176
|
+
bounds_success_tuples = {}
|
158
177
|
def process_chunk_bounds(
|
159
178
|
chunk_begin_and_end: Tuple[
|
160
179
|
Union[int, datetime],
|
161
180
|
Union[int, datetime]
|
162
181
|
]
|
163
182
|
):
|
183
|
+
if chunk_begin_and_end in bounds_success_tuples:
|
184
|
+
return chunk_begin_and_end, bounds_success_tuples[chunk_begin_and_end]
|
185
|
+
|
164
186
|
chunk_begin, chunk_end = chunk_begin_and_end
|
165
187
|
return chunk_begin_and_end, self.sync(
|
166
188
|
begin = chunk_begin,
|
@@ -171,14 +193,25 @@ def verify(
|
|
171
193
|
**kwargs
|
172
194
|
)
|
173
195
|
|
174
|
-
###
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
196
|
+
### If we have more than one chunk, attempt to sync the first one and return if its fails.
|
197
|
+
if len(chunk_bounds) > 1:
|
198
|
+
first_chunk_bounds = chunk_bounds[0]
|
199
|
+
(
|
200
|
+
(first_begin, first_end),
|
201
|
+
(first_success, first_msg)
|
202
|
+
) = process_chunk_bounds(first_chunk_bounds)
|
203
|
+
if not first_success:
|
204
|
+
return (
|
205
|
+
first_success,
|
206
|
+
f"\n{first_begin} - {first_end}\n"
|
207
|
+
+ f"Failed to sync first chunk:\n{first_msg}"
|
208
|
+
)
|
209
|
+
bounds_success_tuples[first_chunk_bounds] = (first_success, first_msg)
|
210
|
+
|
211
|
+
bounds_success_tuples.update(dict(pool.map(process_chunk_bounds, chunk_bounds)))
|
179
212
|
bounds_success_bools = {bounds: tup[0] for bounds, tup in bounds_success_tuples.items()}
|
180
213
|
|
181
|
-
message_header = f"{
|
214
|
+
message_header = f"{begin_to_print} - {end_to_print}"
|
182
215
|
if all(bounds_success_bools.values()):
|
183
216
|
msg = get_chunks_success_message(bounds_success_tuples, header=message_header)
|
184
217
|
if deduplicate:
|
@@ -195,18 +228,19 @@ def verify(
|
|
195
228
|
|
196
229
|
chunk_bounds_to_resync = [
|
197
230
|
bounds
|
198
|
-
for bounds, success in zip(chunk_bounds,
|
231
|
+
for bounds, success in zip(chunk_bounds, bounds_success_bools)
|
199
232
|
if not success
|
200
233
|
]
|
201
234
|
bounds_to_print = [
|
202
235
|
f"{bounds[0]} - {bounds[1]}"
|
203
236
|
for bounds in chunk_bounds_to_resync
|
204
237
|
]
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
238
|
+
if bounds_to_print:
|
239
|
+
warn(
|
240
|
+
f"Will resync the following failed chunks:\n "
|
241
|
+
+ '\n '.join(bounds_to_print),
|
242
|
+
stack = False,
|
243
|
+
)
|
210
244
|
|
211
245
|
retry_bounds_success_tuples = dict(pool.map(process_chunk_bounds, chunk_bounds_to_resync))
|
212
246
|
bounds_success_tuples.update(retry_bounds_success_tuples)
|
@@ -289,7 +323,8 @@ def get_chunks_success_message(
|
|
289
323
|
''
|
290
324
|
if num_fails == 0
|
291
325
|
else (
|
292
|
-
f"\n\nFailed to sync {num_fails}
|
326
|
+
f"\n\nFailed to sync {num_fails} chunk"
|
327
|
+
+ ('s' if num_fails != 1 else '') + ":\n"
|
293
328
|
+ '\n'.join([
|
294
329
|
f"{fail_begin} - {fail_end}\n{msg}\n"
|
295
330
|
for (fail_begin, fail_end), (_, msg) in fail_chunk_bounds_tuples.items()
|
meerschaum/plugins/__init__.py
CHANGED
@@ -254,6 +254,9 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
|
|
254
254
|
try:
|
255
255
|
if PLUGINS_INTERNAL_LOCK_PATH.exists():
|
256
256
|
PLUGINS_INTERNAL_LOCK_PATH.unlink()
|
257
|
+
### Sometimes competing threads will delete the lock file at the same time.
|
258
|
+
except FileNotFoundError:
|
259
|
+
pass
|
257
260
|
except Exception as e:
|
258
261
|
if warn:
|
259
262
|
_warn(f"Error cleaning up lockfile {PLUGINS_INTERNAL_LOCK_PATH}:\n{e}")
|
@@ -168,7 +168,7 @@ class Daemon:
|
|
168
168
|
finally:
|
169
169
|
self._log_refresh_timer.cancel()
|
170
170
|
self.rotating_log.close()
|
171
|
-
if self.pid_path.exists():
|
171
|
+
if self.pid is None and self.pid_path.exists():
|
172
172
|
self.pid_path.unlink()
|
173
173
|
|
174
174
|
if keep_daemon_output:
|
@@ -254,7 +254,7 @@ class Daemon:
|
|
254
254
|
return _launch_success_bool, msg
|
255
255
|
|
256
256
|
|
257
|
-
def kill(self, timeout: Optional[int] =
|
257
|
+
def kill(self, timeout: Optional[int] = 8) -> SuccessTuple:
|
258
258
|
"""Forcibly terminate a running daemon.
|
259
259
|
Sends a SIGTERM signal to the process.
|
260
260
|
|
@@ -293,7 +293,7 @@ class Daemon:
|
|
293
293
|
return True, "Success"
|
294
294
|
|
295
295
|
|
296
|
-
def quit(self, timeout:
|
296
|
+
def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
|
297
297
|
"""Gracefully quit a running daemon."""
|
298
298
|
if self.status == 'paused':
|
299
299
|
return self.kill(timeout)
|
@@ -305,10 +305,24 @@ class Daemon:
|
|
305
305
|
|
306
306
|
def pause(
|
307
307
|
self,
|
308
|
-
timeout:
|
309
|
-
check_timeout_interval: float =
|
308
|
+
timeout: Union[int, float, None] = None,
|
309
|
+
check_timeout_interval: Union[float, int, None] = None,
|
310
310
|
) -> SuccessTuple:
|
311
|
-
"""
|
311
|
+
"""
|
312
|
+
Pause the daemon if it is running.
|
313
|
+
|
314
|
+
Parameters
|
315
|
+
----------
|
316
|
+
timeout: Union[float, int, None], default None
|
317
|
+
The maximum number of seconds to wait for a process to suspend.
|
318
|
+
|
319
|
+
check_timeout_interval: Union[float, int, None], default None
|
320
|
+
The number of seconds to wait between checking if the process is still running.
|
321
|
+
|
322
|
+
Returns
|
323
|
+
-------
|
324
|
+
A `SuccessTuple` indicating whether the `Daemon` process was successfully suspended.
|
325
|
+
"""
|
312
326
|
if self.process is None:
|
313
327
|
return False, f"Daemon '{self.daemon_id}' is not running and cannot be paused."
|
314
328
|
|
@@ -320,8 +334,18 @@ class Daemon:
|
|
320
334
|
except Exception as e:
|
321
335
|
return False, f"Failed to pause daemon '{self.daemon_id}':\n{e}"
|
322
336
|
|
323
|
-
|
324
|
-
|
337
|
+
timeout = self.get_timeout_seconds(timeout)
|
338
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
339
|
+
check_timeout_interval
|
340
|
+
)
|
341
|
+
|
342
|
+
psutil = attempt_import('psutil')
|
343
|
+
|
344
|
+
if not timeout:
|
345
|
+
try:
|
346
|
+
success = self.process.status() == 'stopped'
|
347
|
+
except psutil.NoSuchProcess as e:
|
348
|
+
success = True
|
325
349
|
msg = "Success" if success else f"Failed to suspend daemon '{self.daemon_id}'."
|
326
350
|
if success:
|
327
351
|
self._capture_process_timestamp('paused')
|
@@ -329,9 +353,12 @@ class Daemon:
|
|
329
353
|
|
330
354
|
begin = time.perf_counter()
|
331
355
|
while (time.perf_counter() - begin) < timeout:
|
332
|
-
|
333
|
-
self.
|
334
|
-
|
356
|
+
try:
|
357
|
+
if self.process.status() == 'stopped':
|
358
|
+
self._capture_process_timestamp('paused')
|
359
|
+
return True, "Success"
|
360
|
+
except psutil.NoSuchProcess as e:
|
361
|
+
return False, f"Process exited unexpectedly. Was it killed?\n{e}"
|
335
362
|
time.sleep(check_timeout_interval)
|
336
363
|
|
337
364
|
return False, (
|
@@ -342,10 +369,24 @@ class Daemon:
|
|
342
369
|
|
343
370
|
def resume(
|
344
371
|
self,
|
345
|
-
timeout:
|
346
|
-
check_timeout_interval: float =
|
372
|
+
timeout: Union[int, float, None] = None,
|
373
|
+
check_timeout_interval: Union[float, int, None] = None,
|
347
374
|
) -> SuccessTuple:
|
348
|
-
"""
|
375
|
+
"""
|
376
|
+
Resume the daemon if it is paused.
|
377
|
+
|
378
|
+
Parameters
|
379
|
+
----------
|
380
|
+
timeout: Union[float, int, None], default None
|
381
|
+
The maximum number of seconds to wait for a process to resume.
|
382
|
+
|
383
|
+
check_timeout_interval: Union[float, int, None], default None
|
384
|
+
The number of seconds to wait between checking if the process is still stopped.
|
385
|
+
|
386
|
+
Returns
|
387
|
+
-------
|
388
|
+
A `SuccessTuple` indicating whether the `Daemon` process was successfully resumed.
|
389
|
+
"""
|
349
390
|
if self.status == 'running':
|
350
391
|
return True, f"Daemon '{self.daemon_id}' is already running."
|
351
392
|
|
@@ -357,7 +398,12 @@ class Daemon:
|
|
357
398
|
except Exception as e:
|
358
399
|
return False, f"Failed to resume daemon '{self.daemon_id}':\n{e}"
|
359
400
|
|
360
|
-
|
401
|
+
timeout = self.get_timeout_seconds(timeout)
|
402
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
403
|
+
check_timeout_interval
|
404
|
+
)
|
405
|
+
|
406
|
+
if not timeout:
|
361
407
|
success = self.status == 'running'
|
362
408
|
msg = "Success" if success else f"Failed to resume daemon '{self.daemon_id}'."
|
363
409
|
if success:
|
@@ -417,8 +463,8 @@ class Daemon:
|
|
417
463
|
def _send_signal(
|
418
464
|
self,
|
419
465
|
signal_to_send,
|
420
|
-
timeout:
|
421
|
-
check_timeout_interval: float =
|
466
|
+
timeout: Union[float, int, None] = None,
|
467
|
+
check_timeout_interval: Union[float, int, None] = None,
|
422
468
|
) -> SuccessTuple:
|
423
469
|
"""Send a signal to the daemon process.
|
424
470
|
|
@@ -427,13 +473,11 @@ class Daemon:
|
|
427
473
|
signal_to_send:
|
428
474
|
The signal the send to the daemon, e.g. `signals.SIGINT`.
|
429
475
|
|
430
|
-
timeout:
|
476
|
+
timeout: Union[float, int, None], default None
|
431
477
|
The maximum number of seconds to wait for a process to terminate.
|
432
|
-
Defaults to 3.
|
433
478
|
|
434
|
-
check_timeout_interval: float, default
|
479
|
+
check_timeout_interval: Union[float, int, None], default None
|
435
480
|
The number of seconds to wait between checking if the process is still running.
|
436
|
-
Defaults to 0.1.
|
437
481
|
|
438
482
|
Returns
|
439
483
|
-------
|
@@ -444,11 +488,17 @@ class Daemon:
|
|
444
488
|
except Exception as e:
|
445
489
|
return False, f"Failed to send signal {signal_to_send}:\n{traceback.format_exc()}"
|
446
490
|
|
447
|
-
|
491
|
+
timeout = self.get_timeout_seconds(timeout)
|
492
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
493
|
+
check_timeout_interval
|
494
|
+
)
|
495
|
+
|
496
|
+
if not timeout:
|
448
497
|
return True, f"Successfully sent '{signal}' to daemon '{self.daemon_id}'."
|
498
|
+
|
449
499
|
begin = time.perf_counter()
|
450
500
|
while (time.perf_counter() - begin) < timeout:
|
451
|
-
if not self.
|
501
|
+
if not self.status == 'running':
|
452
502
|
return True, "Success"
|
453
503
|
time.sleep(check_timeout_interval)
|
454
504
|
|
@@ -464,8 +514,8 @@ class Daemon:
|
|
464
514
|
raise a `FileExistsError`.
|
465
515
|
"""
|
466
516
|
try:
|
467
|
-
self.path.mkdir(parents=True, exist_ok=
|
468
|
-
_already_exists =
|
517
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
518
|
+
_already_exists = any(os.scandir(self.path))
|
469
519
|
except FileExistsError:
|
470
520
|
_already_exists = True
|
471
521
|
|
@@ -506,8 +556,17 @@ class Daemon:
|
|
506
556
|
if self.process is None:
|
507
557
|
return 'stopped'
|
508
558
|
|
509
|
-
|
510
|
-
|
559
|
+
psutil = attempt_import('psutil')
|
560
|
+
try:
|
561
|
+
if self.process.status() == 'stopped':
|
562
|
+
return 'paused'
|
563
|
+
except psutil.NoSuchProcess:
|
564
|
+
if self.pid_path.exists():
|
565
|
+
try:
|
566
|
+
self.pid_path.unlink()
|
567
|
+
except Exception as e:
|
568
|
+
pass
|
569
|
+
return 'stopped'
|
511
570
|
|
512
571
|
return 'running'
|
513
572
|
|
@@ -804,6 +863,28 @@ class Daemon:
|
|
804
863
|
self.rotating_log.delete()
|
805
864
|
|
806
865
|
|
866
|
+
def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]:
|
867
|
+
"""
|
868
|
+
Return the timeout value to use. Use `--timeout-seconds` if provided,
|
869
|
+
else the configured default (8).
|
870
|
+
"""
|
871
|
+
if isinstance(timeout, (int, float)):
|
872
|
+
return timeout
|
873
|
+
return get_config('jobs', 'timeout_seconds')
|
874
|
+
|
875
|
+
|
876
|
+
def get_check_timeout_interval_seconds(
|
877
|
+
self,
|
878
|
+
check_timeout_interval: Union[int, float, None] = None,
|
879
|
+
) -> Union[int, float]:
|
880
|
+
"""
|
881
|
+
Return the interval value to check the status of timeouts.
|
882
|
+
"""
|
883
|
+
if isinstance(check_timeout_interval, (int, float)):
|
884
|
+
return check_timeout_interval
|
885
|
+
return get_config('jobs', 'check_timeout_interval_seconds')
|
886
|
+
|
887
|
+
|
807
888
|
def __getstate__(self):
|
808
889
|
"""
|
809
890
|
Pickle this Daemon.
|
@@ -14,6 +14,7 @@ from meerschaum.utils.daemon.Daemon import Daemon
|
|
14
14
|
from meerschaum.utils.daemon.Log import Log
|
15
15
|
from meerschaum.utils.daemon.RotatingFile import RotatingFile
|
16
16
|
|
17
|
+
|
17
18
|
def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
18
19
|
"""Parse sysargs and execute a Meerschaum action as a daemon.
|
19
20
|
|
@@ -27,7 +28,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
27
28
|
A SuccessTuple.
|
28
29
|
"""
|
29
30
|
from meerschaum._internal.entry import entry
|
30
|
-
_args =
|
31
|
+
_args = {}
|
31
32
|
if '--name' in sysargs or '--job-name' in sysargs:
|
32
33
|
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
33
34
|
_args = parse_arguments(sysargs)
|
@@ -36,6 +37,38 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
36
37
|
label = shlex.join(filtered_sysargs) if sysargs else None
|
37
38
|
except Exception as e:
|
38
39
|
label = ' '.join(filtered_sysargs) if sysargs else None
|
40
|
+
|
41
|
+
name = _args.get('name', None)
|
42
|
+
daemon = None
|
43
|
+
if name:
|
44
|
+
try:
|
45
|
+
daemon = Daemon(daemon_id=name)
|
46
|
+
except Exception as e:
|
47
|
+
daemon = None
|
48
|
+
|
49
|
+
if daemon is not None:
|
50
|
+
existing_sysargs = daemon.properties['target']['args'][0]
|
51
|
+
existing_kwargs = parse_arguments(existing_sysargs)
|
52
|
+
|
53
|
+
### Remove sysargs because flags are aliased.
|
54
|
+
_ = _args.pop('daemon', None)
|
55
|
+
_ = _args.pop('sysargs', None)
|
56
|
+
_ = _args.pop('filtered_sysargs', None)
|
57
|
+
debug = _args.pop('debug', None)
|
58
|
+
_args['sub_args'] = sorted(_args.get('sub_args', []))
|
59
|
+
_ = existing_kwargs.pop('daemon', None)
|
60
|
+
_ = existing_kwargs.pop('sysargs', None)
|
61
|
+
_ = existing_kwargs.pop('filtered_sysargs', None)
|
62
|
+
_ = existing_kwargs.pop('debug', None)
|
63
|
+
existing_kwargs['sub_args'] = sorted(existing_kwargs.get('sub_args', []))
|
64
|
+
|
65
|
+
### Only run if the kwargs equal or no actions are provided.
|
66
|
+
if existing_kwargs == _args or not _args.get('action', []):
|
67
|
+
return daemon.run(
|
68
|
+
debug = debug,
|
69
|
+
allow_dirty_run = True,
|
70
|
+
)
|
71
|
+
|
39
72
|
success_tuple = run_daemon(
|
40
73
|
entry,
|
41
74
|
filtered_sysargs,
|
@@ -47,6 +80,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
47
80
|
success_tuple = False, str(success_tuple)
|
48
81
|
return success_tuple
|
49
82
|
|
83
|
+
|
50
84
|
def daemon_action(**kw) -> SuccessTuple:
|
51
85
|
"""Execute a Meerschaum action as a daemon."""
|
52
86
|
from meerschaum.utils.packages import run_python_package
|
meerschaum/utils/dataframe.py
CHANGED