dbworkload 0.7.0a1__tar.gz → 0.8.2__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,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: dbworkload
3
- Version: 0.7.0a1
3
+ Version: 0.8.2
4
4
  Summary: Workload framework
5
- Home-page: https://dbworkload.github.io/dbworkload/
6
5
  License: GPLv3+
7
6
  Author: Fabio Ghirardello
8
7
  Requires-Python: >=3.8,<4.0
@@ -45,6 +44,7 @@ Requires-Dist: pyyaml
45
44
  Requires-Dist: sqlparse
46
45
  Requires-Dist: tabulate
47
46
  Requires-Dist: typer[all]
47
+ Project-URL: Homepage, https://dbworkload.github.io/dbworkload/
48
48
  Project-URL: Repository, https://github.com/dbworkload/dbworkload
49
49
  Description-Content-Type: text/markdown
50
50
 
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/python
2
2
 
3
- from .. import __version__
4
3
  import typer
5
4
 
6
- EPILOG = "GitHub: <https://github.com/fabiog1901/dbworkload>"
5
+ from .. import __version__
6
+
7
+ EPILOG = "Docs: <https://dbworkload.github.io/dbworkload/>"
7
8
 
8
9
 
9
10
  class ConnInfo:
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/python
2
2
 
3
- from .. import __version__
4
- from dbworkload.cli.dep import Param, EPILOG, ConnInfo
3
+ import json
4
+ import logging
5
+ import os
6
+ import platform
7
+ import sys
5
8
  from enum import Enum
6
9
  from pathlib import Path
7
10
  from typing import Optional
8
11
  from urllib.parse import urlparse
12
+
13
+ import pandas as pd
14
+ import typer
15
+ import yaml
16
+
9
17
  import dbworkload.cli.util
10
18
  import dbworkload.models.run
11
19
  import dbworkload.models.util
12
20
  import dbworkload.utils.common
13
- import json
14
- import logging
15
- import os
16
- import platform
17
- import sys
18
- import typer
19
- import yaml
20
- import pandas as pd
21
+ from dbworkload.cli.dep import EPILOG, ConnInfo, Param
22
+
23
+ from .. import __version__
21
24
 
22
25
  logger = logging.getLogger("dbworkload")
23
26
 
@@ -97,6 +100,12 @@ def run(
97
100
  help="Duration in seconds. Defaults to <ad infinitum>.",
98
101
  show_default=False,
99
102
  ),
103
+ max_rate: int = typer.Option(
104
+ None,
105
+ "--max-rate",
106
+ show_default=False,
107
+ help="Set the max-rate to have dbworkload manage concurrency. Defaults to None.",
108
+ ),
100
109
  conn_duration: int = typer.Option(
101
110
  None,
102
111
  "-k",
@@ -139,6 +148,11 @@ def run(
139
148
  "--schedule",
140
149
  help="schedule JSON string or filepath to the schedule file.",
141
150
  ),
151
+ histogram_bins: str = typer.Option(
152
+ "5,10,25,50,75,100,125,250,500,750,1000",
153
+ "--bins",
154
+ help="comma separated list of ints defining the histogram bins.",
155
+ ),
142
156
  log_level: LogLevel = Param.LogLevel,
143
157
  ):
144
158
  logger.setLevel(log_level.upper())
@@ -225,6 +239,7 @@ def run(
225
239
 
226
240
  args = load_args(args)
227
241
 
242
+ histogram_bins = histogram_bins.split(",")
228
243
  schedule = load_schedule(schedule)
229
244
 
230
245
  dbworkload.models.run.run(
@@ -237,11 +252,13 @@ def run(
237
252
  conn_info,
238
253
  duration,
239
254
  conn_duration,
255
+ max_rate,
240
256
  args,
241
257
  driver,
242
258
  quiet,
243
259
  save,
244
260
  schedule,
261
+ histogram_bins,
245
262
  log_level.upper(),
246
263
  )
247
264
 
@@ -289,13 +306,18 @@ def load_args(args: str):
289
306
  def load_schedule(schedule_path: str):
290
307
  if schedule_path:
291
308
  if os.path.exists(schedule_path):
292
- return pd.read_csv(schedule_path, header=None).values.tolist()
309
+ df = pd.read_csv(schedule_path, dtype="Int64", comment="#").fillna(0)
310
+ # trasform ramp and duration columns from minutes to seconds
311
+ df[["ramp", "duration"]] = df[["ramp", "duration"]] * 60
312
+
313
+ return df.values.tolist()
293
314
  else:
294
315
  try:
295
316
  return json.loads(schedule_path)
296
317
  except:
297
318
  logger.error(f"couldn't decode {schedule_path} as JSON")
298
319
 
320
+
299
321
  def _version_callback(value: bool) -> None:
300
322
  if value:
301
323
  typer.echo(f"dbworkload : {__version__}")
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/python
2
2
 
3
- from pathlib import Path
4
3
  from enum import Enum
4
+ from pathlib import Path
5
5
  from typing import Optional
6
+
7
+ import typer
8
+
6
9
  import dbworkload.models.run
7
10
  import dbworkload.models.util
8
11
  import dbworkload.utils.common
9
- from dbworkload.cli.dep import Param, EPILOG
10
- import typer
12
+ from dbworkload.cli.dep import EPILOG, Param
11
13
 
12
14
 
13
15
  class Compression(str, Enum):
@@ -1,21 +1,23 @@
1
1
  #!/usr/bin/python
2
2
 
3
- from contextlib import contextmanager
4
- from dbworkload.cli.dep import ConnInfo
5
- import dbworkload.utils.common
3
+ import errno
6
4
  import logging
7
- import logging.handlers
8
5
  import multiprocessing as mp
9
- import numpy as np
6
+ import os
10
7
  import queue
11
8
  import random
12
9
  import signal
13
10
  import sys
14
- import sys
15
- import tabulate
16
- from threading import Thread
17
11
  import time
18
12
  import traceback
13
+ from contextlib import contextmanager
14
+ from threading import Thread
15
+
16
+ import numpy as np
17
+ import tabulate
18
+
19
+ import dbworkload.utils.common
20
+ from dbworkload.cli.dep import ConnInfo
19
21
 
20
22
  # from cassandra.cluster import Cluster, ExecutionProfile, EXEC_PROFILE_DEFAULT, Session
21
23
  # from cassandra.policies import (
@@ -30,6 +32,8 @@ DEFAULT_SLEEP = 3
30
32
  MAX_RETRIES = 3
31
33
  FREQUENCY = 10
32
34
 
35
+ FIFO = "dbworkload.pipe"
36
+
33
37
  logger = logging.getLogger("dbworkload")
34
38
 
35
39
 
@@ -106,10 +110,10 @@ def signal_handler(sig, frame):
106
110
  sys.exit(1)
107
111
 
108
112
  logger.debug("Sent poison pill to all procs")
113
+ os.remove(FIFO)
109
114
 
110
115
 
111
116
  def cycle(iterable, backwards=False):
112
-
113
117
  global current_proc
114
118
 
115
119
  if not backwards:
@@ -121,7 +125,11 @@ def cycle(iterable, backwards=False):
121
125
  return v
122
126
 
123
127
 
124
- def ramp_up(
128
+ # Launch or kill worker threads based on cc_change value.
129
+ # workers are added or removed evenly across all supervisors.
130
+ # If a ramp time is specified, threads creation or destruction
131
+ # will be paced accordingly.
132
+ def launch_or_kill_workers(
125
133
  queues: list,
126
134
  ramp_time: int,
127
135
  cc_change: int,
@@ -129,11 +137,10 @@ def ramp_up(
129
137
  iterations_per_thread,
130
138
  concurrency,
131
139
  ):
132
-
133
140
  if cc_change == 0:
134
141
  return
135
142
 
136
- ramp_interval = ramp_time * 60 / abs(cc_change)
143
+ ramp_interval = ramp_time / abs(cc_change)
137
144
  global thread_id
138
145
 
139
146
  if cc_change > 0:
@@ -164,11 +171,13 @@ def run(
164
171
  conn_info: dict,
165
172
  duration: int,
166
173
  conn_duration: int,
174
+ max_rate: int,
167
175
  args: dict,
168
176
  driver: str,
169
177
  quiet: bool,
170
178
  save: bool,
171
179
  schedule: list,
180
+ histogram_bins: list,
172
181
  log_level: str,
173
182
  ):
174
183
  def gracefully_shutdown(by_keyinterrupt: bool = False):
@@ -188,7 +197,7 @@ def run(
188
197
  logger.error("Timed out")
189
198
  sys.exit(1)
190
199
 
191
- for x in processes.values():
200
+ for x in supervisors.values():
192
201
  if x.is_alive():
193
202
  x.join()
194
203
 
@@ -321,19 +330,34 @@ def run(
321
330
 
322
331
  stats = dbworkload.utils.common.Stats(start_time)
323
332
 
324
- prom = dbworkload.utils.common.Prom(prom_port)
333
+ prom = dbworkload.utils.common.Prom(prom_port, stats, histogram_bins)
325
334
 
326
335
  to_main_q = mp.Queue()
327
336
 
328
337
  global queues
329
- global processes
330
- processes = {}
338
+ global supervisors
339
+ supervisors = {}
331
340
  queues = {}
332
341
 
342
+ # start a separate thread for messages coming in via the pipe
343
+ # echo 5 > dbworkload.pipe # create 5 more connections
344
+ Thread(
345
+ target=listen_to_pipe,
346
+ daemon=True,
347
+ args=(
348
+ queues,
349
+ 0,
350
+ procs,
351
+ None,
352
+ concurrency,
353
+ ),
354
+ ).start()
355
+
356
+ # launch supervisors in a dedicated OS process
333
357
  for x in range(procs):
334
358
  queues[x] = mp.Queue()
335
- processes[x] = mp.Process(
336
- target=proc,
359
+ supervisors[x] = mp.Process(
360
+ target=supervisor,
337
361
  args=(
338
362
  to_main_q,
339
363
  queues[x],
@@ -348,7 +372,7 @@ def run(
348
372
  ),
349
373
  daemon=True,
350
374
  )
351
- processes[x].start()
375
+ supervisors[x].start()
352
376
 
353
377
  # report time happens 2 seconds after the stats are received.
354
378
  # we add this buffer to make sure we get all the stats reports
@@ -365,6 +389,7 @@ def run(
365
389
  current_proc = -1
366
390
  current_cc = 0
367
391
  thread_id = 0
392
+ pause_for_ramp_time = 0
368
393
 
369
394
  iterations_per_thread = None
370
395
  if iterations:
@@ -378,42 +403,67 @@ def run(
378
403
  f"You have requested {iterations} iterations on {concurrency} threads. {iterations} modulo {concurrency} = {iterations%concurrency} iterations will not be executed."
379
404
  )
380
405
 
406
+ # if no schedule was passed, create a schedule with just 1 line
381
407
  if schedule is None:
382
- schedule = [[concurrency, ramp / 60, duration / 60 if duration else duration]]
408
+ schedule = [(concurrency, max_rate, ramp, duration)]
383
409
 
410
+ # loop through all lines in the schedule
384
411
  for i, s in enumerate(schedule):
385
-
386
- cc, ramp_time, dur = s
412
+ cc, max_rate, ramp_time, dur = s
387
413
 
388
414
  # sanitize
389
415
  if dur and ramp_time > dur:
390
416
  ramp_time = dur
391
417
 
392
- logger.debug(
393
- f"Starting schedule {i+1}/{len(schedule)}: cc = {cc}, ramp = {ramp_time}, dur = {dur}"
418
+ logger.info(
419
+ f"Starting schedule {i+1}/{len(schedule)}: cc={cc}, max_rate={max_rate}, ramp={ramp_time}, dur={dur}"
394
420
  )
395
421
 
396
- if dur:
397
- end_schedule_time = time.time() + dur * 60
398
- else:
399
- end_schedule_time = float("inf")
422
+ # always make sure that a duration is specified, even if none was passed
423
+ # in which case it defaults to infinite
424
+ end_schedule_time = time.time() + dur if dur else float("inf")
425
+
426
+ # if max_rate was set instead of concurrency
427
+ # and current_cc = 0,
428
+ # start the workload with 1 thread so that dbworkload
429
+ # has stats to measure on for adding/removing threads
430
+ # as part of the calculations for maintaining
431
+ # the desired max_rate
432
+ if current_cc == 0 and max_rate:
433
+ Thread(
434
+ target=launch_or_kill_workers,
435
+ daemon=True,
436
+ args=(
437
+ queues,
438
+ ramp_time,
439
+ 1,
440
+ procs,
441
+ iterations_per_thread,
442
+ concurrency,
443
+ ),
444
+ ).start()
445
+
446
+ current_cc = 1
447
+
448
+ if not max_rate:
449
+ Thread(
450
+ target=launch_or_kill_workers,
451
+ daemon=True,
452
+ args=(
453
+ queues,
454
+ ramp_time,
455
+ cc - current_cc,
456
+ procs,
457
+ iterations_per_thread,
458
+ concurrency,
459
+ ),
460
+ ).start()
400
461
 
401
- Thread(
402
- target=ramp_up,
403
- daemon=True,
404
- args=(
405
- queues,
406
- ramp_time,
407
- cc - current_cc,
408
- procs,
409
- iterations_per_thread,
410
- concurrency,
411
- ),
412
- ).start()
462
+ current_cc = cc
413
463
 
414
- current_cc = cc
415
464
  returned_threads = 0
416
465
 
466
+ # loop for the entire duration of the schedule's current line
417
467
  while time.time() < end_schedule_time:
418
468
  try:
419
469
  # read from the queue for stats or completion messages
@@ -459,6 +509,55 @@ def run(
459
509
 
460
510
  report = stats.calculate_stats(active_connections, endtime)
461
511
 
512
+ # if max_rate is specified, try to stick to it.
513
+ # to calculate how to get to the max rate, we need a non-empty report
514
+ if max_rate and report:
515
+ current_rate = report[0][6] # __cycle__ period_ops/s
516
+
517
+ # approximate how many threads are needed to get
518
+ # to the desired max_rate given the current QPS rate
519
+ # and current threads count
520
+ extrapolated_cc = int(max_rate / (current_rate / current_cc))
521
+
522
+ # adjust the thread count if there is a difference
523
+ # between the current thread count and the calculated
524
+ # thread count, but not if there is one such operation already
525
+ # running, that is, not if there's an operation that is slow due
526
+ # to a long ramp_time.
527
+ if (
528
+ extrapolated_cc - current_cc
529
+ and time.time() >= pause_for_ramp_time
530
+ ):
531
+ Thread(
532
+ target=launch_or_kill_workers,
533
+ daemon=True,
534
+ args=(
535
+ queues,
536
+ ramp_time,
537
+ extrapolated_cc - current_cc,
538
+ procs,
539
+ iterations_per_thread,
540
+ concurrency,
541
+ ),
542
+ ).start()
543
+
544
+ # make sure we will not add/remove threads while the newly
545
+ # created thread is still working
546
+ pause_for_ramp_time = time.time() + ramp_time + 2 * FREQUENCY
547
+
548
+ logger.warning(
549
+ f"Calculating max_rate: desired max_rate: {max_rate}, "
550
+ f"current_rate: {report[0][6]}, current_cc = {current_cc}, "
551
+ f"extrapolated_cc = {extrapolated_cc}, "
552
+ f"difference: {extrapolated_cc-current_cc}"
553
+ )
554
+ current_cc = extrapolated_cc
555
+
556
+ # ramp_time is only considered for reaching the desired max_rate.
557
+ # For adjustments over time, we want the changes to happen immediately
558
+ # and not smoothed out over the initial ramp_time value
559
+ ramp_time = 0
560
+
462
561
  centroids = stats.get_centroids()
463
562
 
464
563
  stats.new_window(endtime)
@@ -486,7 +585,15 @@ def run(
486
585
  gracefully_shutdown()
487
586
 
488
587
 
489
- def proc(
588
+ # a supervisor runs in a separate process.
589
+ # The idea is to create as many supervisors as vCPUs.
590
+ # The sole role of the supervisor is to listen for instructions
591
+ # from the MainProcess.
592
+ # Instructions are:
593
+ # - Create a new worker.
594
+ # - Destroy a worker.
595
+ # - Destroy all workers and return.
596
+ def supervisor(
490
597
  to_main_q: mp.Queue,
491
598
  from_main_q: mp.Queue,
492
599
  log_level: str,
@@ -498,7 +605,6 @@ def proc(
498
605
  offset: int,
499
606
  id: int,
500
607
  ):
501
-
502
608
  def gracefully_return(msg):
503
609
  # wait for Threads to return before
504
610
  # letting the Process MainThread return
@@ -518,48 +624,42 @@ def proc(
518
624
  return
519
625
 
520
626
  logger.setLevel(log_level)
521
-
522
627
  logger.debug(f"PROC-{id} started")
523
628
 
524
629
  threads: list[Thread] = []
525
-
526
630
  from_proc_q = mp.Queue()
527
631
 
528
632
  # capture KeyboardInterrupt and do nothing
529
633
  signal.signal(signal.SIGINT, signal.SIG_IGN)
530
634
 
531
635
  while True:
532
- try:
533
- msg = from_main_q.get(block=True)
534
-
535
- if msg == "proc_end":
536
- logger.debug(f"PROC-{id} terminating...")
537
- gracefully_return("proc_returned")
538
- return
539
- elif msg == "kill_one":
540
- from_proc_q.put("poison_pill")
541
- elif isinstance(msg, tuple):
542
- t = Thread(
543
- target=worker,
544
- daemon=True,
545
- args=(
546
- to_main_q,
547
- from_proc_q,
548
- log_level,
549
- conn_info,
550
- driver,
551
- workload,
552
- args,
553
- conn_duration,
554
- offset,
555
- *msg,
556
- ),
557
- )
558
- t.start()
559
- threads.append(t)
560
-
561
- except queue.Empty:
562
- pass
636
+ msg = from_main_q.get(block=True)
637
+
638
+ if msg == "proc_end":
639
+ logger.debug(f"PROC-{id} terminating...")
640
+ gracefully_return("proc_returned")
641
+ return
642
+ elif msg == "kill_one":
643
+ from_proc_q.put("poison_pill")
644
+ elif isinstance(msg, tuple):
645
+ t = Thread(
646
+ target=worker,
647
+ daemon=True,
648
+ args=(
649
+ to_main_q,
650
+ from_proc_q,
651
+ log_level,
652
+ conn_info,
653
+ driver,
654
+ workload,
655
+ args,
656
+ conn_duration,
657
+ offset,
658
+ *msg,
659
+ ),
660
+ )
661
+ t.start()
662
+ threads.append(t)
563
663
 
564
664
 
565
665
  def worker(
@@ -576,7 +676,6 @@ def worker(
576
676
  iterations: int = 0,
577
677
  concurrency: int = 0,
578
678
  ):
579
-
580
679
  def gracefully_return(msg):
581
680
  # send notification to MainThread
582
681
  to_main_q.put(msg)
@@ -732,6 +831,38 @@ def worker(
732
831
  return
733
832
 
734
833
 
834
+ def listen_to_pipe(queues, ramp_time, procs, iterations_per_thread, concurrency):
835
+ # https://stackoverflow.com/questions/39089776/python-read-named-pipe
836
+
837
+ try:
838
+ os.mkfifo(FIFO)
839
+ except OSError as oe:
840
+ if oe.errno != errno.EEXIST:
841
+ raise
842
+
843
+ while True:
844
+ with open(FIFO) as fifo:
845
+ for line in fifo:
846
+ try:
847
+ t = int(line)
848
+ except:
849
+ continue
850
+
851
+ logger.info(f"{'Adding' if t > 0 else 'Removing' } {abs(t)} threads.")
852
+ Thread(
853
+ target=launch_or_kill_workers,
854
+ daemon=True,
855
+ args=(
856
+ queues,
857
+ ramp_time,
858
+ t,
859
+ procs,
860
+ iterations_per_thread,
861
+ concurrency,
862
+ ),
863
+ ).start()
864
+
865
+
735
866
  def log_and_sleep(e: Exception):
736
867
  logger.error(f"error_type={e.__class__.__name__}, msg={e}")
737
868
  logger.info("Sleeping for %s seconds" % (DEFAULT_SLEEP))
@@ -1,28 +1,29 @@
1
1
  #!/usr/bin/python
2
2
 
3
- from io import TextIOWrapper
4
- from jinja2 import Environment, PackageLoader
5
- from pathlib import PosixPath
6
- from plotly.subplots import make_subplots
7
- from pytdigest import TDigest
8
3
  import datetime as dt
9
- import dbworkload
10
- import dbworkload.utils.common
11
- import dbworkload.utils.simplefaker
12
4
  import gzip
13
5
  import itertools
14
6
  import logging
15
- import numpy as np
16
7
  import os
8
+ import shutil
9
+ import sys
10
+ from io import TextIOWrapper
11
+ from pathlib import PosixPath
12
+
13
+ import numpy as np
17
14
  import pandas as pd
18
15
  import plotext as plt
19
16
  import plotly.graph_objects as go
20
17
  import plotly.io as pio
21
- import shutil
22
18
  import sqlparse
23
- import sys
24
19
  import yaml
20
+ from jinja2 import Environment, PackageLoader
21
+ from plotly.subplots import make_subplots
22
+ from pytdigest import TDigest
25
23
 
24
+ import dbworkload
25
+ import dbworkload.utils.common
26
+ import dbworkload.utils.simplefaker
26
27
 
27
28
  logger = logging.getLogger("dbworkload")
28
29
  logger.setLevel(logging.INFO)
@@ -54,7 +55,9 @@ def util_csv(
54
55
  if os.path.isdir(output_dir):
55
56
  os.rename(
56
57
  output_dir,
57
- str(output_dir) + "." + dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S"),
58
+ str(output_dir)
59
+ + "."
60
+ + dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S"),
58
61
  )
59
62
 
60
63
  # create new directory
@@ -102,7 +105,12 @@ def util_yaml(input: PosixPath, output: PosixPath):
102
105
 
103
106
  # backup the current file as to not override
104
107
  if os.path.exists(output):
105
- os.rename(output, str(output) + "." + dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S"))
108
+ os.rename(
109
+ output,
110
+ str(output)
111
+ + "."
112
+ + dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S"),
113
+ )
106
114
 
107
115
  # create new file
108
116
  with open(output, "w") as f:
@@ -2,14 +2,17 @@
2
2
 
3
3
  import importlib
4
4
  import logging
5
- import numpy as np
6
5
  import os
7
6
  import random
8
7
  import sys
9
8
  import time
10
9
  import urllib.parse
11
- import yaml
10
+
11
+ import numpy as np
12
12
  import prometheus_client as prom
13
+ import yaml
14
+ from prometheus_client.core import REGISTRY, HistogramMetricFamily
15
+ from prometheus_client.registry import Collector
13
16
  from pytdigest import TDigest
14
17
 
15
18
  RESERVED_WORDS = [
@@ -45,57 +48,8 @@ NOT_NULL_MAX = 40
45
48
 
46
49
  logger = logging.getLogger("dbworkload")
47
50
 
48
-
49
- class Prom:
50
- def __init__(self, prom_port: int = 26260):
51
- self.prom_latency: dict[str, list[prom.Gauge]] = {}
52
-
53
- # don't stop just because prom server can't start
54
- try:
55
- prom.start_http_server(prom_port)
56
- except OSError as e:
57
- logger.warning(f"Cannot start prometheus server: {e}")
58
-
59
- self.threads = prom.Gauge(
60
- "threads", "count of connection threads to the database."
61
- )
62
-
63
- def publish(self, report: list):
64
- for row in report:
65
- id = row[1]
66
-
67
- if id not in self.prom_latency:
68
- self.prom_latency[id] = []
69
- self.prom_latency[id].append(
70
- prom.Gauge(f"{id}__tot_ops", "total count of ops")
71
- )
72
- self.prom_latency[id].append(
73
- prom.Gauge(
74
- f"{id}__tot_ops_s", "derived value from tot_ops / elapsed"
75
- )
76
- )
77
- self.prom_latency[id].append(
78
- prom.Gauge(f"{id}__period_ops", "ops count for the recent window")
79
- )
80
- self.prom_latency[id].append(
81
- prom.Gauge(
82
- f"{id}__period_ops_s",
83
- "derived value from period_ops / window duration",
84
- )
85
- )
86
- self.prom_latency[id].append(prom.Gauge(f"{id}__mean_ms", "mean_ms"))
87
- self.prom_latency[id].append(prom.Gauge(f"{id}__p50_ms", "p50_ms"))
88
- self.prom_latency[id].append(prom.Gauge(f"{id}__p90_ms", "p90_ms"))
89
- self.prom_latency[id].append(prom.Gauge(f"{id}__p95_ms", "p95_ms"))
90
- self.prom_latency[id].append(prom.Gauge(f"{id}__p99_ms", "p99_ms"))
91
- self.prom_latency[id].append(prom.Gauge(f"{id}__max_ms", "max_ms"))
92
-
93
- for idx, v in enumerate(row[3:]):
94
- self.prom_latency[id][idx].set(v)
95
-
96
- # threads value is the same for all rows
97
- if report:
98
- self.threads.set(report[0][2])
51
+ from prometheus_client.core import REGISTRY, HistogramMetricFamily
52
+ from prometheus_client.registry import Collector
99
53
 
100
54
 
101
55
  class Stats:
@@ -216,6 +170,84 @@ class WorkerStats:
216
170
  ]
217
171
 
218
172
 
173
+ class CustomHistogram(Collector):
174
+ def __init__(self, name: str, stats: Stats, bins: list):
175
+ self.name = name
176
+ self.stats = stats
177
+ self.bins = bins
178
+
179
+ def get_buckets(self, name):
180
+ td = self.stats.cumulative_counts.get(name)
181
+ if td is None:
182
+ return [["+Inf", 0]]
183
+
184
+ # create buckets from 10 ... 180
185
+ td_hist = [[x, int(td.cdf((int(x) + 1) / 1000) * td.weight)] for x in self.bins]
186
+ td_hist.append(["+Inf", td.weight])
187
+
188
+ return td.mean * 1000 * td.weight, td_hist
189
+
190
+ def collect(self):
191
+ sum_value, buckets = self.get_buckets(self.name)
192
+ yield HistogramMetricFamily(
193
+ f"{self.name}_latency_ms",
194
+ f"Latency in ms for {self.name}",
195
+ buckets,
196
+ sum_value,
197
+ )
198
+
199
+
200
+ class Prom:
201
+ def __init__(self, prom_port: int = 26260, stats: Stats = None, bins: list = []):
202
+ self.prom_latency: dict[str, list[prom.Gauge]] = {}
203
+ self.stats = stats
204
+ self.bins = bins
205
+
206
+ # don't stop just because prom server can't start
207
+ try:
208
+ prom.start_http_server(prom_port)
209
+ except OSError as e:
210
+ logger.warning(f"Cannot start prometheus server: {e}")
211
+
212
+ self.threads = prom.Gauge(
213
+ "threads", "count of connection threads to the database."
214
+ )
215
+
216
+ def publish(self, report: list, td: dict = {}):
217
+ for row in report:
218
+ id = row[1]
219
+
220
+ if id not in self.prom_latency:
221
+ self.prom_latency[id] = []
222
+
223
+ REGISTRY.register(CustomHistogram(id, self.stats, self.bins))
224
+
225
+ self.prom_latency[id].append(
226
+ prom.Gauge(f"{id}__tot_ops", "total count of ops")
227
+ )
228
+ self.prom_latency[id].append(
229
+ prom.Gauge(
230
+ f"{id}__tot_ops_s", "derived value from tot_ops / elapsed"
231
+ )
232
+ )
233
+ self.prom_latency[id].append(
234
+ prom.Gauge(f"{id}__period_ops", "ops count for the recent window")
235
+ )
236
+ self.prom_latency[id].append(
237
+ prom.Gauge(
238
+ f"{id}__period_ops_s",
239
+ "derived value from period_ops / window duration",
240
+ )
241
+ )
242
+
243
+ for idx, v in enumerate(row[3:6]):
244
+ self.prom_latency[id][idx].set(v)
245
+
246
+ # threads value is the same for all rows
247
+ if report:
248
+ self.threads.set(report[0][2])
249
+
250
+
219
251
  def get_driver_from_scheme(scheme: str):
220
252
  return {
221
253
  "postgres": "postgres",
@@ -654,23 +686,6 @@ def ddl_to_yaml(ddl: str):
654
686
  elif within_brackets > 0 and i == ",":
655
687
  col_def += ":"
656
688
 
657
- # process the content within parenthesis in the
658
- # CREATE TABLE stmt char by char to distinguish
659
- # the comma for separating columns vs the comma
660
- # included in single quote strings such as those in DEFAULT
661
- # eg: mycol STRING NULL DEFAULT 'corporate, inc'
662
- within_quote = False
663
- col_def_str = col_def
664
- col_def = ""
665
- for i in col_def_str:
666
- if i == "'":
667
- within_quote = not within_quote
668
- continue
669
- if within_quote:
670
- continue
671
- else:
672
- col_def += i
673
-
674
689
  col_def = [x.strip().lower() for x in col_def.split(",")]
675
690
 
676
691
  ll = []
@@ -1,12 +1,14 @@
1
+ import builtins
1
2
  import csv
2
3
  import datetime as dt
3
4
  import logging
4
5
  import multiprocessing as mp
5
6
  import os
6
- import pandas as pd
7
- import uuid
8
7
  import random
9
- import builtins
8
+ import uuid
9
+
10
+ import pandas as pd
11
+
10
12
  from .common import import_class_at_runtime
11
13
 
12
14
  logger = logging.getLogger("dbworkload")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dbworkload"
3
- version = "0.7.0a1"
3
+ version = "0.8.2"
4
4
  description = "Workload framework"
5
5
  authors = ["Fabio Ghirardello"]
6
6
  license = "GPLv3+"
File without changes
File without changes