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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbworkload
3
- Version: 0.8.4
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
- FIFO = "dbworkload.pipe"
37
+ DBWORKLOAD_PIPE = "dbworkload.pipe"
38
38
 
39
39
  logger = logging.getLogger("dbworkload")
40
40
 
41
- force_exit = False
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 force_exit
102
- if force_exit:
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
- force_exit = True
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(by_keyinterrupt: bool = False):
179
+ def gracefully_shutdown():
195
180
  logger.debug("Gracefully shutting down...")
196
181
 
197
182
  end_time = int(time.time())
198
- # _s = stats_received
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
- if not by_keyinterrupt:
201
- for q in queues.values():
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
- q.put("proc_end")
204
- except queue.Full:
205
- logger.error("Timed out")
206
- sys.exit(1)
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
- for x in supervisors.values():
209
- if x.is_alive():
210
- x.join()
201
+ x.join()
211
202
 
212
- # Commenting the below as in theory there shouldn't be any stats that
213
- # comes in *after* the PROC returns. That is, all threads send stats
214
- # when all threads are returned, then the supervisor returns.
215
- # while True:
216
- # try:
217
- # msg = to_main_q.get(block=True, timeout=2.0)
218
- # if isinstance(msg, list):
219
- # _s += 1
220
- # stats.add_tds(msg)
221
- # if _s >= active_connections:
222
- # break
223
- # else:
224
- # logger.error("Timed out, quitting")
225
- # sys.exit(1)
226
-
227
- # except queue.Empty:
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, end_time),
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={cc}, max_rate={max_rate}, ramp={ramp_time}, dur={dur}"
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
- returned_threads = 0
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
- returned_threads += 1
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={msg}")
510
- sys.exit(1)
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
- sys.exit(1)
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"PROC-{id} started")
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 == "proc_end":
655
- logger.debug(f"PROC-{id} terminating...")
656
- gracefully_return("proc_returned")
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
- logger.debug(f"Thread ID {id} terminated")
682
+ # send notification to MainThread
683
+ to_main_q.put(msg)
702
684
 
703
- return
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
- return gracefully_return("got_killed")
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(FIFO)
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(FIFO) as fifo:
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.8.4"
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