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.
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/PKG-INFO +3 -3
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/cli/dep.py +3 -2
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/cli/main.py +33 -11
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/cli/util.py +5 -3
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/models/run.py +208 -77
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/models/util.py +21 -13
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/utils/common.py +85 -70
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/utils/simplefaker.py +5 -3
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/pyproject.toml +1 -1
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/LICENSE +0 -0
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/README.md +0 -0
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/__init__.py +0 -0
- {dbworkload-0.7.0a1 → dbworkload-0.8.2}/dbworkload/templates/stub.j2 +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: dbworkload
|
|
3
|
-
Version: 0.
|
|
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,23 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
target=
|
|
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
|
-
|
|
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 = [
|
|
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.
|
|
393
|
-
f"Starting schedule {i+1}/{len(schedule)}: cc
|
|
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
|
|
397
|
-
|
|
398
|
-
else
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|