dbworkload 0.9.1__tar.gz → 0.9.2.dev1__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,17 +1,14 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbworkload
3
- Version: 0.9.1
3
+ Version: 0.9.2.dev1
4
4
  Summary: Workload framework
5
5
  License: GPLv3+
6
6
  Author: Fabio Ghirardello
7
- Requires-Python: >=3.8,<4.0
7
+ Requires-Python: >=3.11,<4.0
8
8
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
12
  Classifier: Programming Language :: Python :: 3.11
16
13
  Classifier: Programming Language :: Python :: 3.12
17
14
  Classifier: Programming Language :: Python :: 3.13
@@ -2,6 +2,7 @@
2
2
 
3
3
  import errno
4
4
  import logging
5
+ import math
5
6
  import multiprocessing as mp
6
7
  import os
7
8
  import queue
@@ -19,6 +20,7 @@ from psutil import cpu_percent, virtual_memory
19
20
 
20
21
  import dbworkload.utils.common
21
22
  from dbworkload.cli.dep import ConnInfo
23
+ from dbworkload.utils.common import Action
22
24
 
23
25
  # from cassandra.cluster import Cluster, ExecutionProfile, EXEC_PROFILE_DEFAULT, Session
24
26
  # from cassandra.policies import (
@@ -40,6 +42,9 @@ logger = logging.getLogger("dbworkload")
40
42
 
41
43
  sigterm_received = False
42
44
 
45
+ supervisors: dict[int, mp.Process] = {}
46
+ supervisor_queues: dict[int, mp.Queue] = {}
47
+
43
48
  HEADERS: list = [
44
49
  "elapsed",
45
50
  "id",
@@ -125,7 +130,7 @@ def cycle(iterable, backwards=False):
125
130
  # If a ramp time is specified, threads creation or destruction
126
131
  # will be paced accordingly.
127
132
  def launch_or_kill_workers(
128
- queues: list,
133
+ supervisor_queues: list,
129
134
  ramp_time: int,
130
135
  cc_change: int,
131
136
  proc_len: list,
@@ -140,11 +145,14 @@ def launch_or_kill_workers(
140
145
 
141
146
  if cc_change > 0:
142
147
  for _ in range(cc_change):
143
- queues[cycle(proc_len)].put(
148
+ supervisor_queues[cycle(proc_len)].put(
144
149
  (
145
- thread_id,
146
- iterations_per_thread,
147
- concurrency,
150
+ Action.NEW_WORKER,
151
+ (
152
+ thread_id,
153
+ iterations_per_thread,
154
+ concurrency,
155
+ ),
148
156
  )
149
157
  )
150
158
  thread_id += 1
@@ -152,7 +160,7 @@ def launch_or_kill_workers(
152
160
 
153
161
  if cc_change < 0:
154
162
  for _ in range(abs(cc_change)):
155
- queues[cycle(proc_len, backwards=True)].put("kill_one")
163
+ supervisor_queues[cycle(proc_len, backwards=True)].put((Action.KILL_ONE,))
156
164
  time.sleep(ramp_interval)
157
165
 
158
166
 
@@ -183,8 +191,8 @@ def run(
183
191
  _stats_received = stats_received
184
192
 
185
193
  # notify all Supervisors to quit
186
- for q in queues.values():
187
- q.put("poison_pill")
194
+ for q in supervisor_queues.values():
195
+ q.put((Action.POISON_PILL,))
188
196
 
189
197
  # wait for supervisors to quit and drain
190
198
  # the to_main_q at the same time to avoid locking
@@ -339,10 +347,8 @@ def run(
339
347
 
340
348
  to_main_q = mp.Queue()
341
349
 
342
- global queues
350
+ global supervisor_queues
343
351
  global supervisors
344
- supervisors = {}
345
- queues = {}
346
352
 
347
353
  # start a separate thread for messages coming in via the pipe
348
354
  # echo 5 > dbworkload.pipe # create 5 more connections
@@ -350,7 +356,7 @@ def run(
350
356
  target=listen_to_pipe,
351
357
  daemon=True,
352
358
  args=(
353
- queues,
359
+ supervisor_queues,
354
360
  0,
355
361
  procs,
356
362
  None,
@@ -360,12 +366,12 @@ def run(
360
366
 
361
367
  # launch supervisors in a dedicated OS process
362
368
  for x in range(procs):
363
- queues[x] = mp.Queue()
369
+ supervisor_queues[x] = mp.Queue()
364
370
  supervisors[x] = mp.Process(
365
371
  target=supervisor,
366
372
  args=(
367
373
  to_main_q,
368
- queues[x],
374
+ supervisor_queues[x],
369
375
  log_level,
370
376
  conn_info,
371
377
  driver,
@@ -394,7 +400,6 @@ def run(
394
400
  current_proc = -1
395
401
  current_cc = 0
396
402
  thread_id = 0
397
- pause_for_ramp_time = 0
398
403
 
399
404
  iterations_per_thread = None
400
405
  if iterations:
@@ -424,13 +429,17 @@ def run(
424
429
  f"Starting schedule {i+1}/{len(schedule)}: {cc=}, {max_rate=}, {ramp_time=}, {dur=}"
425
430
  )
426
431
 
432
+ pause_for_ramp_time = time.time() + 3 * FREQUENCY
433
+ exhaust_warning = 0
434
+ think_time = 0
435
+
427
436
  # always make sure that a duration is specified, even if none was passed
428
437
  # in which case it defaults to infinite
429
438
  end_schedule_time = time.time() + dur if dur else float("inf")
430
439
 
431
440
  # if max_rate was set instead of concurrency
432
441
  # and current_cc = 0,
433
- # start the workload with 1 thread so that dbworkload
442
+ # start the workload with 10 threads so that dbworkload
434
443
  # has stats to measure on for adding/removing threads
435
444
  # as part of the calculations for maintaining
436
445
  # the desired max_rate
@@ -439,23 +448,23 @@ def run(
439
448
  target=launch_or_kill_workers,
440
449
  daemon=True,
441
450
  args=(
442
- queues,
451
+ supervisor_queues,
443
452
  ramp_time,
444
- 1,
453
+ 10,
445
454
  procs,
446
455
  iterations_per_thread,
447
456
  concurrency,
448
457
  ),
449
458
  ).start()
450
459
 
451
- current_cc = 1
460
+ current_cc = 10
452
461
 
453
462
  if not max_rate:
454
463
  Thread(
455
464
  target=launch_or_kill_workers,
456
465
  daemon=True,
457
466
  args=(
458
- queues,
467
+ supervisor_queues,
459
468
  ramp_time,
460
469
  cc - current_cc,
461
470
  procs,
@@ -518,49 +527,131 @@ def run(
518
527
  if max_rate and report:
519
528
  current_rate = report[0][6] # __cycle__ period_ops/s
520
529
 
521
- # approximate how many threads are needed to get
522
- # to the desired max_rate given the current QPS rate
523
- # and current threads count
524
- extrapolated_cc = int(max_rate / (current_rate / current_cc))
525
-
526
- # adjust the thread count if there is a difference
527
- # between the current thread count and the calculated
528
- # thread count, but not if there is one such operation already
529
- # running, that is, not if there's an operation that is slow due
530
- # to a long ramp_time.
531
- if (
532
- extrapolated_cc - current_cc
533
- and time.time() >= pause_for_ramp_time
534
- ):
535
- Thread(
536
- target=launch_or_kill_workers,
537
- daemon=True,
538
- args=(
539
- queues,
540
- ramp_time,
541
- extrapolated_cc - current_cc,
542
- procs,
543
- iterations_per_thread,
544
- concurrency,
545
- ),
546
- ).start()
547
-
548
- # make sure we will not add/remove threads while the newly
549
- # created thread is still working
550
- pause_for_ramp_time = time.time() + ramp_time + 2 * FREQUENCY
551
-
552
- logger.warning(
553
- f"Calculating max_rate: desired max_rate: {max_rate}, "
554
- f"current_rate: {report[0][6]}, current_cc = {current_cc}, "
555
- f"extrapolated_cc = {extrapolated_cc}, "
556
- f"difference: {extrapolated_cc-current_cc}"
557
- )
558
- current_cc = extrapolated_cc
559
-
560
- # ramp_time is only considered for reaching the desired max_rate.
561
- # For adjustments over time, we want the changes to happen immediately
562
- # and not smoothed out over the initial ramp_time value
563
- ramp_time = 0
530
+ if time.time() > pause_for_ramp_time:
531
+
532
+ if current_rate < max_rate * 0.99:
533
+ if think_time > 0:
534
+
535
+ think_time = (
536
+ math.floor(
537
+ think_time * current_rate / max_rate * 1000
538
+ )
539
+ / 1000
540
+ )
541
+
542
+ pause_for_ramp_time = time.time() + 3 * FREQUENCY
543
+
544
+ logger.info(
545
+ f"Calculating max_rate: {max_rate=} {current_rate=}, {current_cc=} {think_time=}"
546
+ )
547
+
548
+ else:
549
+ if exhaust_warning > 3:
550
+ logger.warning("Pointless to add any more threads")
551
+ else:
552
+
553
+ # approximate how many threads are needed to get
554
+ # to the desired max_rate given the current QPS rate
555
+ # and current thread count
556
+ # increase by 5%
557
+ extrapolated_cc = int(
558
+ max_rate / (current_rate / current_cc) * 1.05
559
+ )
560
+
561
+ exhaust_warning += 1
562
+
563
+ change = max(0, extrapolated_cc - current_cc)
564
+
565
+ Thread(
566
+ target=launch_or_kill_workers,
567
+ daemon=True,
568
+ args=(
569
+ supervisor_queues,
570
+ ramp_time,
571
+ change,
572
+ procs,
573
+ iterations_per_thread,
574
+ concurrency,
575
+ ),
576
+ ).start()
577
+
578
+ # give enough time for newly created threads to settle
579
+ # before new calculations are performed
580
+ pause_for_ramp_time = (
581
+ time.time() + ramp_time + 6 * FREQUENCY
582
+ )
583
+
584
+ logger.info(
585
+ f"Calculating max_rate: {max_rate=} {current_rate=}, {current_cc=} {change=}"
586
+ )
587
+ current_cc += change
588
+
589
+ # ramp_time is only considered for reaching the desired max_rate.
590
+ # For adjustments over time, we want the changes to happen immediately
591
+ # and not smoothed out over the initial ramp_time value
592
+ ramp_time = 0
593
+
594
+ for q in supervisor_queues.values():
595
+ q.put((Action.THINK_TIME, think_time))
596
+
597
+ elif current_rate * 0.99 > max_rate:
598
+ if think_time > 1:
599
+ # pointless to add more time, remove threads instead
600
+
601
+ # decrease count by 5%
602
+ change = int(current_cc * -0.05)
603
+
604
+ Thread(
605
+ target=launch_or_kill_workers,
606
+ daemon=True,
607
+ args=(
608
+ supervisor_queues,
609
+ ramp_time,
610
+ change,
611
+ procs,
612
+ iterations_per_thread,
613
+ concurrency,
614
+ ),
615
+ ).start()
616
+
617
+ # give enough time for newly created threads to settle
618
+ # before new calculations are performed
619
+ pause_for_ramp_time = (
620
+ time.time() + ramp_time + 6 * FREQUENCY
621
+ )
622
+
623
+ logger.info(
624
+ f"Calculating max_rate: {max_rate=} {current_rate=}, {current_cc=} {change=}"
625
+ )
626
+ current_cc += change
627
+
628
+ # ramp_time is only considered for reaching the desired max_rate.
629
+ # For adjustments over time, we want the changes to happen immediately
630
+ # and not smoothed out over the initial ramp_time value
631
+ ramp_time = 0
632
+ think_time = 0
633
+
634
+ else:
635
+ # add think_time to slow it down a bit
636
+
637
+ if think_time == 0:
638
+
639
+ think_time = round(
640
+ 0.01 * current_rate / max_rate, 4
641
+ )
642
+ else:
643
+ think_time = round(
644
+ think_time * current_rate / max_rate, 4
645
+ )
646
+
647
+ pause_for_ramp_time = time.time() + 3 * FREQUENCY
648
+
649
+ logger.info(
650
+ f"Calculating max_rate: {max_rate=} {current_rate=}, {current_cc=} {think_time=}"
651
+ )
652
+
653
+ for q in supervisor_queues.values():
654
+ q.put((Action.THINK_TIME, think_time))
564
655
 
565
656
  centroids = stats.get_centroids()
566
657
 
@@ -612,53 +703,60 @@ def supervisor(
612
703
  logger.setLevel(log_level)
613
704
  logger.debug(f"Supervisor-{id} started")
614
705
 
615
- threads: list[Thread] = []
706
+ worker_threads: list[Thread] = []
616
707
  from_proc_q = mp.Queue()
617
708
 
618
709
  # capture KeyboardInterrupt and do nothing
619
710
  signal.signal(signal.SIGINT, signal.SIG_IGN)
620
711
 
712
+ global think_time
713
+ think_time = 0
714
+
621
715
  while True:
622
716
  msg = from_main_q.get(block=True)
623
717
 
624
- if msg == "poison_pill":
625
- logger.debug(f"Supervisor-{id} terminating...")
718
+ match msg[0]:
719
+ case Action.POISON_PILL: # poison pill
720
+ logger.debug(f"Supervisor-{id} terminating...")
626
721
 
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")
722
+ # wait for Threads to return before
723
+ # letting the Supervisor MainThread return
724
+ for x in worker_threads:
725
+ if x.is_alive():
726
+ from_proc_q.put(Action.POISON_PILL)
632
727
 
633
- for x in threads:
634
- if x.is_alive():
635
- x.join()
728
+ for x in worker_threads:
729
+ if x.is_alive():
730
+ x.join()
636
731
 
637
- logger.debug(f"Supervisor-{id} terminated")
638
- return
732
+ logger.debug(f"Supervisor-{id} terminated")
733
+ return
639
734
 
640
- elif msg == "kill_one":
641
- from_proc_q.put("poison_pill")
735
+ case Action.KILL_ONE: # kill_one
736
+ from_proc_q.put(Action.POISON_PILL)
642
737
 
643
- elif isinstance(msg, tuple):
644
- t = Thread(
645
- target=worker,
646
- daemon=True,
647
- args=(
648
- to_main_q,
649
- from_proc_q,
650
- log_level,
651
- conn_info,
652
- driver,
653
- workload,
654
- args,
655
- conn_duration,
656
- offset,
657
- *msg,
658
- ),
659
- )
660
- t.start()
661
- threads.append(t)
738
+ case Action.THINK_TIME: # set think_time
739
+ think_time = msg[1]
740
+
741
+ case Action.NEW_WORKER: # add new worker
742
+ t = Thread(
743
+ target=worker,
744
+ daemon=True,
745
+ args=(
746
+ to_main_q,
747
+ from_proc_q,
748
+ log_level,
749
+ conn_info,
750
+ driver,
751
+ workload,
752
+ args,
753
+ conn_duration,
754
+ offset,
755
+ *msg[1],
756
+ ),
757
+ )
758
+ t.start()
759
+ worker_threads.append(t)
662
760
 
663
761
 
664
762
  def worker(
@@ -795,6 +893,10 @@ def worker(
795
893
 
796
894
  ws.add_latency_measurement("__cycle__", time.time() - cycle_start)
797
895
 
896
+ if think_time > 0:
897
+ time.sleep(think_time)
898
+ ws.add_latency_measurement("__think_time__", think_time)
899
+
798
900
  if to_main_q.full():
799
901
  logger.error("=========== Q FULL!!!! ======================")
800
902
  if time.time() >= stat_time:
@@ -871,6 +973,7 @@ def listen_to_pipe(queues, ramp_time, procs, iterations_per_thread, concurrency)
871
973
 
872
974
 
873
975
  def log_and_sleep(e: Exception):
976
+ raise e
874
977
  logger.error(f"error_type={e.__class__.__name__}, msg={e}")
875
978
  logger.info("Sleeping for %s seconds" % (DEFAULT_SLEEP))
876
979
  time.sleep(DEFAULT_SLEEP)
@@ -48,10 +48,19 @@ NOT_NULL_MAX = 40
48
48
 
49
49
  logger = logging.getLogger("dbworkload")
50
50
 
51
+ from enum import IntEnum
52
+
51
53
  from prometheus_client.core import REGISTRY, HistogramMetricFamily
52
54
  from prometheus_client.registry import Collector
53
55
 
54
56
 
57
+ class Action(IntEnum):
58
+ KILL_ONE = 7
59
+ THINK_TIME = 2
60
+ POISON_PILL = 9
61
+ NEW_WORKER = 1
62
+
63
+
55
64
  class Stats:
56
65
  """Print workload stats
57
66
  and export the stats as Prometheus endpoints
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dbworkload"
3
- version = "0.9.1"
3
+ version = "0.9.2.dev1"
4
4
  description = "Workload framework"
5
5
  authors = ["Fabio Ghirardello"]
6
6
  license = "GPLv3+"
@@ -17,7 +17,7 @@ classifiers = [
17
17
  dbworkload = 'dbworkload.cli.main:app'
18
18
 
19
19
  [tool.poetry.dependencies]
20
- python = "^3.8"
20
+ python = "^3.11"
21
21
  pandas = "*"
22
22
  tabulate = "*"
23
23
  numpy = "*"
@@ -51,6 +51,11 @@ mongo = ["pymongo"]
51
51
  cassandra = ["cassandra-driver"]
52
52
  spanner = ["google-cloud-spanner"]
53
53
 
54
+ [tool.poetry.group.dev.dependencies]
55
+ mkdocs = "^1.6.1"
56
+ mkdocs-material = "^9.6.14"
57
+ mkdocs-click = "^0.9.0"
58
+
54
59
  [build-system]
55
60
  requires = ["poetry-core"]
56
61
  build-backend = "poetry.core.masonry.api"
File without changes
File without changes