dbworkload 0.8.4__tar.gz → 0.9.1__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.
- {dbworkload-0.8.4 → dbworkload-0.9.1}/PKG-INFO +2 -1
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/cli/main.py +4 -1
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/models/run.py +99 -108
- {dbworkload-0.8.4 → dbworkload-0.9.1}/pyproject.toml +2 -1
- {dbworkload-0.8.4 → dbworkload-0.9.1}/LICENSE +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/README.md +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/__init__.py +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/cli/dep.py +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/cli/util.py +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/models/util.py +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/templates/stub.j2 +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/utils/common.py +0 -0
- {dbworkload-0.8.4 → dbworkload-0.9.1}/dbworkload/utils/simplefaker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dbworkload
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Workload framework
|
|
5
5
|
License: GPLv3+
|
|
6
6
|
Author: Fabio Ghirardello
|
|
@@ -35,6 +35,7 @@ Requires-Dist: pandas
|
|
|
35
35
|
Requires-Dist: plotext
|
|
36
36
|
Requires-Dist: plotly
|
|
37
37
|
Requires-Dist: prometheus-client
|
|
38
|
+
Requires-Dist: psutil (>=7.0.0,<8.0.0)
|
|
38
39
|
Requires-Dist: psycopg ; extra == "all" or extra == "postgres"
|
|
39
40
|
Requires-Dist: psycopg-binary ; extra == "all" or extra == "postgres"
|
|
40
41
|
Requires-Dist: pymongo ; extra == "all" or extra == "mongo"
|
|
@@ -16,7 +16,6 @@ import yaml
|
|
|
16
16
|
|
|
17
17
|
import dbworkload.cli.util
|
|
18
18
|
import dbworkload.models.run
|
|
19
|
-
import dbworkload.models.util
|
|
20
19
|
import dbworkload.utils.common
|
|
21
20
|
from dbworkload.cli.dep import EPILOG, ConnInfo, Param
|
|
22
21
|
|
|
@@ -153,6 +152,9 @@ def run(
|
|
|
153
152
|
"--bins",
|
|
154
153
|
help="comma separated list of ints defining the histogram bins.",
|
|
155
154
|
),
|
|
155
|
+
delay_stats: int = typer.Option(
|
|
156
|
+
0, "--delay-stats", help="Start collecting stats after the speciied seconds."
|
|
157
|
+
),
|
|
156
158
|
log_level: LogLevel = Param.LogLevel,
|
|
157
159
|
):
|
|
158
160
|
logger.setLevel(log_level.upper())
|
|
@@ -259,6 +261,7 @@ def run(
|
|
|
259
261
|
save,
|
|
260
262
|
schedule,
|
|
261
263
|
histogram_bins,
|
|
264
|
+
delay_stats,
|
|
262
265
|
log_level.upper(),
|
|
263
266
|
)
|
|
264
267
|
|
|
@@ -34,11 +34,11 @@ MAX_RETRIES = 3
|
|
|
34
34
|
FREQUENCY = 10
|
|
35
35
|
STATS_BUFFER = 8
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
DBWORKLOAD_PIPE = "dbworkload.pipe"
|
|
38
38
|
|
|
39
39
|
logger = logging.getLogger("dbworkload")
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
sigterm_received = False
|
|
42
42
|
|
|
43
43
|
HEADERS: list = [
|
|
44
44
|
"elapsed",
|
|
@@ -98,30 +98,14 @@ def signal_handler(sig, frame):
|
|
|
98
98
|
frame (_type_):
|
|
99
99
|
"""
|
|
100
100
|
logger.info("KeyboardInterrupt signal detected. Stopping processes...")
|
|
101
|
-
global
|
|
102
|
-
|
|
101
|
+
global sigterm_received
|
|
102
|
+
|
|
103
|
+
# if a keyboardinterrupt event was already receive, just exit.
|
|
104
|
+
if sigterm_received:
|
|
103
105
|
logger.warning("Forcibly quitting. You're rude!")
|
|
104
106
|
sys.exit(1)
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# send the poison pill to each proc.
|
|
109
|
-
# if dbworkload cannot graceful shutdown due
|
|
110
|
-
# to processes being still in the init phase
|
|
111
|
-
# when the pill is sent, a subsequent Ctrl+C will cause
|
|
112
|
-
# the pill to overflow the kill_q
|
|
113
|
-
# and raise the queue.Full exception, forcing to quit.
|
|
114
|
-
for q in queues.values():
|
|
115
|
-
try:
|
|
116
|
-
q.put("proc_end")
|
|
117
|
-
except queue.Full:
|
|
118
|
-
logger.error("Timed out")
|
|
119
|
-
sys.exit(1)
|
|
120
|
-
|
|
121
|
-
logger.debug("Sent poison pill to all procs")
|
|
122
|
-
|
|
123
|
-
if os.path.exists(FIFO):
|
|
124
|
-
os.remove(FIFO)
|
|
108
|
+
sigterm_received = True
|
|
125
109
|
|
|
126
110
|
|
|
127
111
|
def cycle(iterable, backwards=False):
|
|
@@ -189,46 +173,52 @@ def run(
|
|
|
189
173
|
save: bool,
|
|
190
174
|
schedule: list,
|
|
191
175
|
histogram_bins: list,
|
|
176
|
+
delay_stats: int,
|
|
192
177
|
log_level: str,
|
|
193
178
|
):
|
|
194
|
-
def gracefully_shutdown(
|
|
179
|
+
def gracefully_shutdown():
|
|
195
180
|
logger.debug("Gracefully shutting down...")
|
|
196
181
|
|
|
197
182
|
end_time = int(time.time())
|
|
198
|
-
|
|
183
|
+
_stats_received = stats_received
|
|
184
|
+
|
|
185
|
+
# notify all Supervisors to quit
|
|
186
|
+
for q in queues.values():
|
|
187
|
+
q.put("poison_pill")
|
|
199
188
|
|
|
200
|
-
|
|
201
|
-
|
|
189
|
+
# wait for supervisors to quit and drain
|
|
190
|
+
# the to_main_q at the same time to avoid locking
|
|
191
|
+
for x in supervisors.values():
|
|
192
|
+
while x.is_alive():
|
|
202
193
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
msg = to_main_q.get(block=True, timeout=0.5)
|
|
195
|
+
if isinstance(msg, list):
|
|
196
|
+
_stats_received += 1
|
|
197
|
+
stats.add_tds(msg)
|
|
198
|
+
except queue.Empty:
|
|
199
|
+
pass
|
|
207
200
|
|
|
208
|
-
|
|
209
|
-
if x.is_alive():
|
|
210
|
-
x.join()
|
|
201
|
+
x.join()
|
|
211
202
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# break
|
|
203
|
+
# Catch all for loose stats, if any?
|
|
204
|
+
while True:
|
|
205
|
+
try:
|
|
206
|
+
msg = to_main_q.get(block=False)
|
|
207
|
+
if isinstance(msg, list):
|
|
208
|
+
_stats_received += 1
|
|
209
|
+
stats.add_tds(msg)
|
|
210
|
+
except queue.Empty:
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
cpu_util = cpu_percent()
|
|
214
|
+
vmem = virtual_memory().percent
|
|
215
|
+
if _stats_received != active_connections or cpu_util > 70 or vmem > 70:
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"{_stats_received=}, expected={active_connections}. CPU Util={cpu_util}%, Memory={vmem}%"
|
|
218
|
+
)
|
|
229
219
|
|
|
230
220
|
# now that we have all stat reports, calculate the stats one last time.
|
|
231
|
-
report = stats.calculate_stats(active_connections, end_time)
|
|
221
|
+
report = stats.calculate_stats(active_connections, end_time - delay_stats)
|
|
232
222
|
centroids = stats.get_centroids()
|
|
233
223
|
|
|
234
224
|
if save:
|
|
@@ -250,7 +240,7 @@ def run(
|
|
|
250
240
|
|
|
251
241
|
# the final stat report summarizes the entire test run
|
|
252
242
|
final_stats_report = tabulate.tabulate(
|
|
253
|
-
stats.calculate_final_stats(active_connections,
|
|
243
|
+
stats.calculate_final_stats(active_connections, stats.endtime),
|
|
254
244
|
FINAL_HEADERS,
|
|
255
245
|
tablefmt="simple_outline",
|
|
256
246
|
intfmt=",",
|
|
@@ -268,6 +258,7 @@ def run(
|
|
|
268
258
|
["iterations", iterations],
|
|
269
259
|
["ramp", ramp],
|
|
270
260
|
["args", args],
|
|
261
|
+
["delay_stats", delay_stats],
|
|
271
262
|
],
|
|
272
263
|
headers=["Parameter", "Value"],
|
|
273
264
|
)
|
|
@@ -312,6 +303,9 @@ def run(
|
|
|
312
303
|
sep="",
|
|
313
304
|
)
|
|
314
305
|
|
|
306
|
+
if os.path.exists(DBWORKLOAD_PIPE):
|
|
307
|
+
os.remove(DBWORKLOAD_PIPE)
|
|
308
|
+
|
|
315
309
|
sys.exit(0)
|
|
316
310
|
|
|
317
311
|
logger.setLevel(log_level)
|
|
@@ -388,7 +382,7 @@ def run(
|
|
|
388
382
|
# report time happens STATS_BUFFER seconds after the stats are received.
|
|
389
383
|
# we add this buffer to make sure we get all the stats reports
|
|
390
384
|
# from each thread before we aggregate and display
|
|
391
|
-
report_time = start_time + FREQUENCY + STATS_BUFFER
|
|
385
|
+
report_time = start_time + FREQUENCY + STATS_BUFFER + delay_stats
|
|
392
386
|
|
|
393
387
|
returned_procs = 0
|
|
394
388
|
active_connections = 0
|
|
@@ -427,7 +421,7 @@ def run(
|
|
|
427
421
|
ramp_time = dur
|
|
428
422
|
|
|
429
423
|
logger.info(
|
|
430
|
-
f"Starting schedule {i+1}/{len(schedule)}: cc=
|
|
424
|
+
f"Starting schedule {i+1}/{len(schedule)}: {cc=}, {max_rate=}, {ramp_time=}, {dur=}"
|
|
431
425
|
)
|
|
432
426
|
|
|
433
427
|
# always make sure that a duration is specified, even if none was passed
|
|
@@ -472,7 +466,7 @@ def run(
|
|
|
472
466
|
|
|
473
467
|
current_cc = cc
|
|
474
468
|
|
|
475
|
-
|
|
469
|
+
task_done_threads = 0
|
|
476
470
|
|
|
477
471
|
# loop for the entire duration of the schedule's current line
|
|
478
472
|
while time.time() < end_schedule_time:
|
|
@@ -487,30 +481,24 @@ def run(
|
|
|
487
481
|
active_connections += 1
|
|
488
482
|
elif msg == "got_killed":
|
|
489
483
|
active_connections -= 1
|
|
490
|
-
elif msg == "proc_returned":
|
|
491
|
-
returned_procs += 1
|
|
492
|
-
logger.debug(f"Stopped processes: {returned_procs}/{procs} ")
|
|
493
484
|
elif msg == "task_done":
|
|
494
|
-
|
|
495
|
-
except queue.Empty:
|
|
496
|
-
pass
|
|
497
|
-
|
|
498
|
-
# check if all procs returned, then exit
|
|
499
|
-
if returned_procs >= procs or (
|
|
500
|
-
returned_threads > 0 and returned_threads >= active_connections
|
|
501
|
-
):
|
|
502
|
-
if msg == "task_done":
|
|
503
|
-
logger.info("Requested iteration/duration limit reached")
|
|
504
|
-
gracefully_shutdown()
|
|
505
|
-
elif msg == "proc_returned":
|
|
506
|
-
logger.debug("All procs returned")
|
|
507
|
-
gracefully_shutdown(by_keyinterrupt=True)
|
|
485
|
+
task_done_threads += 1
|
|
508
486
|
elif isinstance(msg, Exception):
|
|
509
|
-
logger.error(f"error_type={msg.__class__.__name__}, msg=
|
|
510
|
-
|
|
487
|
+
logger.error(f"error_type={msg.__class__.__name__}, {msg=}")
|
|
488
|
+
gracefully_shutdown()
|
|
511
489
|
else:
|
|
512
490
|
logger.error(f"unrecognized message: {msg}")
|
|
513
|
-
|
|
491
|
+
gracefully_shutdown()
|
|
492
|
+
|
|
493
|
+
except queue.Empty:
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
if sigterm_received:
|
|
497
|
+
gracefully_shutdown()
|
|
498
|
+
|
|
499
|
+
if task_done_threads > 0 and task_done_threads >= active_connections:
|
|
500
|
+
logger.info("Requested iteration/duration limit reached")
|
|
501
|
+
gracefully_shutdown()
|
|
514
502
|
|
|
515
503
|
if time.time() >= report_time:
|
|
516
504
|
cpu_util = cpu_percent()
|
|
@@ -521,7 +509,7 @@ def run(
|
|
|
521
509
|
)
|
|
522
510
|
|
|
523
511
|
# remove the STATS_BUFFER seconds added
|
|
524
|
-
endtime = int(time.time()) - STATS_BUFFER
|
|
512
|
+
endtime = int(time.time() - delay_stats) - STATS_BUFFER
|
|
525
513
|
|
|
526
514
|
report = stats.calculate_stats(active_connections, endtime)
|
|
527
515
|
|
|
@@ -621,26 +609,8 @@ def supervisor(
|
|
|
621
609
|
offset: int,
|
|
622
610
|
id: int,
|
|
623
611
|
):
|
|
624
|
-
def gracefully_return(msg):
|
|
625
|
-
# wait for Threads to return before
|
|
626
|
-
# letting the Process MainThread return
|
|
627
|
-
# threading.enumerate()
|
|
628
|
-
for x in threads:
|
|
629
|
-
if x.is_alive():
|
|
630
|
-
from_proc_q.put("poison_pill")
|
|
631
|
-
|
|
632
|
-
for x in threads:
|
|
633
|
-
if x.is_alive():
|
|
634
|
-
x.join()
|
|
635
|
-
|
|
636
|
-
# send notification to MainThread
|
|
637
|
-
to_main_q.put(msg)
|
|
638
|
-
|
|
639
|
-
logger.debug(f"PROC-{id} terminated")
|
|
640
|
-
return
|
|
641
|
-
|
|
642
612
|
logger.setLevel(log_level)
|
|
643
|
-
logger.debug(f"
|
|
613
|
+
logger.debug(f"Supervisor-{id} started")
|
|
644
614
|
|
|
645
615
|
threads: list[Thread] = []
|
|
646
616
|
from_proc_q = mp.Queue()
|
|
@@ -651,12 +621,25 @@ def supervisor(
|
|
|
651
621
|
while True:
|
|
652
622
|
msg = from_main_q.get(block=True)
|
|
653
623
|
|
|
654
|
-
if msg == "
|
|
655
|
-
logger.debug(f"
|
|
656
|
-
|
|
624
|
+
if msg == "poison_pill":
|
|
625
|
+
logger.debug(f"Supervisor-{id} terminating...")
|
|
626
|
+
|
|
627
|
+
# wait for Threads to return before
|
|
628
|
+
# letting the Supervisor MainThread return
|
|
629
|
+
for x in threads:
|
|
630
|
+
if x.is_alive():
|
|
631
|
+
from_proc_q.put("poison_pill")
|
|
632
|
+
|
|
633
|
+
for x in threads:
|
|
634
|
+
if x.is_alive():
|
|
635
|
+
x.join()
|
|
636
|
+
|
|
637
|
+
logger.debug(f"Supervisor-{id} terminated")
|
|
657
638
|
return
|
|
639
|
+
|
|
658
640
|
elif msg == "kill_one":
|
|
659
641
|
from_proc_q.put("poison_pill")
|
|
642
|
+
|
|
660
643
|
elif isinstance(msg, tuple):
|
|
661
644
|
t = Thread(
|
|
662
645
|
target=worker,
|
|
@@ -693,14 +676,13 @@ def worker(
|
|
|
693
676
|
concurrency: int = 0,
|
|
694
677
|
):
|
|
695
678
|
def gracefully_return(msg):
|
|
696
|
-
# send notification to MainThread
|
|
697
|
-
to_main_q.put(msg)
|
|
698
679
|
# send final stats
|
|
699
680
|
to_main_q.put(ws.get_tdigest_ndarray(), block=False)
|
|
700
681
|
|
|
701
|
-
|
|
682
|
+
# send notification to MainThread
|
|
683
|
+
to_main_q.put(msg)
|
|
702
684
|
|
|
703
|
-
|
|
685
|
+
logger.debug(f"Thread ID {id} returned")
|
|
704
686
|
|
|
705
687
|
logger.setLevel(log_level)
|
|
706
688
|
|
|
@@ -726,6 +708,15 @@ def worker(
|
|
|
726
708
|
to_main_q.put("init")
|
|
727
709
|
|
|
728
710
|
while True:
|
|
711
|
+
# listen for termination messages (poison pill)
|
|
712
|
+
try:
|
|
713
|
+
from_proc_q.get(block=False)
|
|
714
|
+
logger.debug("Poison pill received, terminating...")
|
|
715
|
+
gracefully_return("got_killed")
|
|
716
|
+
return
|
|
717
|
+
except queue.Empty:
|
|
718
|
+
pass
|
|
719
|
+
|
|
729
720
|
if conn_duration:
|
|
730
721
|
# reconnect every conn_duration +/- 20%
|
|
731
722
|
conn_endtime = time.time() + int(conn_duration * random.uniform(0.8, 1.2))
|
|
@@ -741,7 +732,6 @@ def worker(
|
|
|
741
732
|
run_init = False
|
|
742
733
|
|
|
743
734
|
if hasattr(w, "setup") and callable(w.setup):
|
|
744
|
-
logger.debug("Executing setup() function")
|
|
745
735
|
run_transaction(
|
|
746
736
|
conn,
|
|
747
737
|
lambda conn: w.setup(
|
|
@@ -761,8 +751,9 @@ def worker(
|
|
|
761
751
|
# listen for termination messages (poison pill)
|
|
762
752
|
try:
|
|
763
753
|
from_proc_q.get(block=False)
|
|
764
|
-
logger.debug("Poison pill received")
|
|
765
|
-
|
|
754
|
+
logger.debug("Poison pill received, terminating...")
|
|
755
|
+
gracefully_return("got_killed")
|
|
756
|
+
return
|
|
766
757
|
except queue.Empty:
|
|
767
758
|
pass
|
|
768
759
|
|
|
@@ -851,13 +842,13 @@ def listen_to_pipe(queues, ramp_time, procs, iterations_per_thread, concurrency)
|
|
|
851
842
|
# https://stackoverflow.com/questions/39089776/python-read-named-pipe
|
|
852
843
|
|
|
853
844
|
try:
|
|
854
|
-
os.mkfifo(
|
|
845
|
+
os.mkfifo(DBWORKLOAD_PIPE)
|
|
855
846
|
except OSError as oe:
|
|
856
847
|
if oe.errno != errno.EEXIST:
|
|
857
848
|
raise
|
|
858
849
|
|
|
859
850
|
while True:
|
|
860
|
-
with open(
|
|
851
|
+
with open(DBWORKLOAD_PIPE) as fifo:
|
|
861
852
|
for line in fifo:
|
|
862
853
|
try:
|
|
863
854
|
t = int(line)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "dbworkload"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Workload framework"
|
|
5
5
|
authors = ["Fabio Ghirardello"]
|
|
6
6
|
license = "GPLv3+"
|
|
@@ -38,6 +38,7 @@ plotext = "*"
|
|
|
38
38
|
plotly = "*"
|
|
39
39
|
jinja2 = "*"
|
|
40
40
|
sqlparse = "*"
|
|
41
|
+
psutil = "^7.0.0"
|
|
41
42
|
|
|
42
43
|
[tool.poetry.extras]
|
|
43
44
|
all = ["psycopg", "psycopg-binary", "mysql-connector-python", "mariadb", "oracledb", "pyodbc", "pymongo", "cassandra-driver", "google-cloud-spanner"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|