Skip to content

Commit e8338f4

Browse files
committed
Address SLO review comments
1 parent e9f1f7e commit e8338f4

9 files changed

Lines changed: 178 additions & 46 deletions

File tree

.github/workflows/slo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ jobs:
137137
uses: ydb-platform/ydb-slo-action/init@v2
138138
timeout-minutes: 30
139139
with:
140-
github_issue: ${{ github.event.inputs.github_issue }}
140+
github_issue: ${{ github.event.pull_request.number || inputs.github_issue }}
141141
github_token: ${{ secrets.GITHUB_TOKEN }}
142142
workload_name: ${{ matrix.sdk.name }}
143143
workload_duration: ${{ inputs.slo_workload_duration_seconds || '600' }}

tests/slo/slo_runner.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ set -euo pipefail
2323
# MESSAGE_SIZE : topic message size bytes (default: 100)
2424
# REPORT_PERIOD_MS: metrics flush period ms (default: 1000)
2525
# DEBUG : 1 to enable --debug for workload (default: 0)
26+
# ASYNC : 1 to enable --async for workload (default: 0)
2627

2728
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2829
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
@@ -39,6 +40,7 @@ WRITE_THREADS="${WRITE_THREADS:-1}"
3940
MESSAGE_SIZE="${MESSAGE_SIZE:-100}"
4041
REPORT_PERIOD_MS="${REPORT_PERIOD_MS:-1000}"
4142
DEBUG="${DEBUG:-0}"
43+
ASYNC="${ASYNC:-0}"
4244

4345
WORKLOAD_IMAGE="${WORKLOAD_IMAGE:-ydb-python-slo:local}"
4446

@@ -78,9 +80,14 @@ build_workload_command() {
7880
--read-threads "${READ_THREADS}"
7981
--write-threads "${WRITE_THREADS}"
8082
--write-rps "${WRITE_RPS}"
81-
--message-size "${MESSAGE_SIZE}"
8283
--time "${RUN_TIME_SEC}"
8384
)
85+
if [[ "${WORKLOAD_NAME}" == "topic" ]]; then
86+
cmd+=(--message-size "${MESSAGE_SIZE}")
87+
if [[ "${ASYNC}" == "1" ]]; then
88+
cmd+=(--async)
89+
fi
90+
fi
8491
if [[ "${DEBUG}" == "1" ]]; then
8592
cmd+=(--debug)
8693
fi

tests/slo/src/core/metrics.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ def __init__(self, workload_name: str, workload_ref: str):
132132
resource = Resource.create(
133133
{
134134
"service.name": f"workload-{workload_name}",
135-
"service.instance.id": environ.get(
136-
"SLO_INSTANCE_ID", f"{workload_ref}-{workload_name}"
137-
),
135+
"service.instance.id": environ.get("SLO_INSTANCE_ID", f"{workload_ref}-{workload_name}"),
138136
"ref": workload_ref,
139137
"sdk": "ydb-python-sdk",
140138
"sdk_version": version("ydb"),
@@ -227,9 +225,7 @@ def stop(
227225

228226
self._retry_attempts_total.add(int(attempts), attributes=base_attrs)
229227
self._pending.add(-1, attributes=base_attrs)
230-
self._operations_total.add(
231-
1, attributes={**base_attrs, "operation_status": op_status}
232-
)
228+
self._operations_total.add(1, attributes={**base_attrs, "operation_status": op_status})
233229

234230
if error is not None:
235231
self._errors.add(
@@ -291,7 +287,5 @@ def create_metrics(args) -> BaseMetrics:
291287
try:
292288
return OtlpMetrics(args.workload_name, args.workload_ref)
293289
except Exception:
294-
logger.exception(
295-
"Failed to initialize OTLP metrics — falling back to DummyMetrics"
296-
)
290+
logger.exception("Failed to initialize OTLP metrics — falling back to DummyMetrics")
297291
return DummyMetrics()

tests/slo/src/jobs/async_topic_jobs.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,12 @@ async def _run_topic_read_jobs(self):
5858

5959
return tasks
6060

61-
async def _run_topic_writes(
62-
self, limiter: AsyncLimiter, partition_id: Optional[int] = None
63-
):
61+
async def _run_topic_writes(self, limiter: AsyncLimiter, partition_id: Optional[int] = None):
6462
start_time = time.time()
6563
logger.info("Start async topic write workload")
6664

6765
async with self.aio_driver.topic_client.writer(
68-
self.args.path,
66+
self.args.topic_path,
6967
codec=getattr(ydb, "PublicCodec", ydb.TopicCodec).GZIP,
7068
partition_id=partition_id,
7169
) as writer:
@@ -100,8 +98,8 @@ async def _run_topic_reads(self, limiter: AsyncLimiter):
10098
logger.info("Start async topic read workload")
10199

102100
async with self.aio_driver.topic_client.reader(
103-
self.args.path,
104-
self.args.consumer,
101+
self.args.topic_path,
102+
self.args.topic_consumer,
105103
) as reader:
106104
logger.info("Async topic reader created")
107105

tests/slo/src/jobs/topic_jobs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def _run_topic_writes(self, limiter, partition_id=None):
6666
logger.info("Start topic write workload")
6767

6868
with self.driver.topic_client.writer(
69-
self.args.path,
69+
self.args.topic_path,
7070
codec=ydb.TopicCodec.GZIP,
7171
partition_id=partition_id,
7272
) as writer:
@@ -100,8 +100,8 @@ def _run_topic_reads(self, limiter):
100100
logger.info("Start topic read workload")
101101

102102
with self.driver.topic_client.reader(
103-
self.args.path,
104-
self.args.consumer,
103+
self.args.topic_path,
104+
self.args.topic_consumer,
105105
) as reader:
106106
logger.info("Topic reader created")
107107

tests/slo/src/options.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
import argparse
22
import os
3+
import sys
4+
5+
_WORKLOADS = {"sync-table", "sync-query", "topic"}
6+
7+
_TABLE_OPTIONS = {
8+
"--table-name",
9+
"--min-partitions-count",
10+
"--max-partitions-count",
11+
"--partition-size",
12+
"--initial-data-count",
13+
"--batch-size",
14+
"--threads",
15+
}
16+
17+
_TOPIC_OPTIONS = {
18+
"--topic-path",
19+
"--topic-consumer",
20+
"--topic-partitions",
21+
"--message-size",
22+
}
23+
24+
25+
def _provided_options(argv):
26+
options = set()
27+
for arg in argv:
28+
option = arg.split("=", 1)[0]
29+
if option.startswith("--"):
30+
options.add(option)
31+
return options
32+
33+
34+
def _validate_options(parser, args, provided_options):
35+
if args.workload_name not in _WORKLOADS:
36+
parser.error(f"Unknown workload-name: {args.workload_name}. Expected one of: {', '.join(sorted(_WORKLOADS))}")
37+
38+
if args.workload_name == "topic":
39+
invalid_options = sorted(provided_options & _TABLE_OPTIONS)
40+
else:
41+
invalid_options = sorted(provided_options & _TOPIC_OPTIONS)
42+
43+
if invalid_options:
44+
parser.error(f"{args.workload_name} workload does not accept options: {', '.join(invalid_options)}")
45+
46+
if args.async_mode and args.workload_name != "topic":
47+
parser.error("--async is supported only for topic workload")
348

449

550
def parse_options():
@@ -48,7 +93,12 @@ def parse_options():
4893
)
4994

5095
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
51-
parser.add_argument("--async", dest="async_mode", action="store_true", help="Run workload in async mode")
96+
parser.add_argument(
97+
"--async",
98+
dest="async_mode",
99+
action="store_true",
100+
help="Run workload in async mode",
101+
)
52102

53103
# Table params
54104
parser.add_argument("--table-name", default="key_value", help="Table name")
@@ -81,10 +131,7 @@ def parse_options():
81131
parser.add_argument("--message-size", default=100, type=int, help="Topic message size [bytes]")
82132

83133
args = parser.parse_args()
84-
85-
# Aliases used by topic runner
86-
args.path = args.topic_path
87-
args.consumer = args.topic_consumer
88-
args.partitions_count = args.topic_partitions
134+
provided_options = _provided_options(sys.argv[1:])
135+
_validate_options(parser, args, provided_options)
89136

90137
return args

tests/slo/src/root_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ async def _run_all_async(args):
7777

7878
try:
7979
logger.info("[%s][async] Creating resources", workload_name)
80-
runner.create(args)
80+
await runner.create_async(args)
8181

8282
logger.info("[%s][async] Running workload for %d s", workload_name, args.time)
8383
await runner.run_async(args)
8484
finally:
8585
logger.info("[%s][async] Cleaning up resources", workload_name)
8686
try:
87-
runner.cleanup(args)
87+
await runner.cleanup_async(args)
8888
except Exception:
8989
logger.exception("Cleanup failed — ignoring")

tests/slo/src/runners/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def set_driver(self, driver: ydb.Driver):
2222
def create(self, args):
2323
pass
2424

25+
async def create_async(self, args):
26+
raise NotImplementedError(f"Async create is not supported for {self.prefix}")
27+
2528
@abstractmethod
2629
def run(self, args):
2730
pass
@@ -32,3 +35,6 @@ async def run_async(self, args):
3235
@abstractmethod
3336
def cleanup(self, args):
3437
pass
38+
39+
async def cleanup_async(self, args):
40+
raise NotImplementedError(f"Async cleanup is not supported for {self.prefix}")

tests/slo/src/runners/topic_runner.py

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import time
23

34
from core.metrics import create_metrics
@@ -19,36 +20,42 @@ def create(self, args):
1920
assert self.driver is not None, "Driver is not initialized. Call set_driver() before create()."
2021
retry_no = 0
2122
while retry_no < 3:
22-
self.logger.info("Creating topic: %s (retry no: %d)", args.path, retry_no)
23+
self.logger.info("Creating topic: %s (retry no: %d)", args.topic_path, retry_no)
2324

2425
try:
2526
self.driver.topic_client.create_topic(
26-
path=args.path,
27-
min_active_partitions=args.partitions_count,
28-
max_active_partitions=args.partitions_count,
29-
consumers=[args.consumer],
27+
path=args.topic_path,
28+
min_active_partitions=args.topic_partitions,
29+
max_active_partitions=args.topic_partitions,
30+
consumers=[args.topic_consumer],
3031
)
3132

32-
self.logger.info("Topic created successfully: %s", args.path)
33-
self.logger.info("Consumer created: %s", args.consumer)
33+
self.logger.info("Topic created successfully: %s", args.topic_path)
34+
self.logger.info("Consumer created: %s", args.topic_consumer)
3435
return
3536

3637
except ydb.Error as e:
3738
error_msg = str(e).lower()
3839
if "already exists" in error_msg:
39-
self.logger.info("Topic already exists: %s", args.path)
40+
self.logger.info("Topic already exists: %s", args.topic_path)
4041

4142
try:
42-
description = self.driver.topic_client.describe_topic(args.path)
43-
consumer_exists = any(c.name == args.consumer for c in description.consumers)
43+
description = self.driver.topic_client.describe_topic(args.topic_path)
44+
consumer_exists = any(c.name == args.topic_consumer for c in description.consumers)
4445

4546
if not consumer_exists:
46-
self.logger.info("Adding consumer %s to existing topic", args.consumer)
47-
self.driver.topic_client.alter_topic(path=args.path, add_consumers=[args.consumer])
48-
self.logger.info("Consumer added successfully: %s", args.consumer)
47+
self.logger.info(
48+
"Adding consumer %s to existing topic",
49+
args.topic_consumer,
50+
)
51+
self.driver.topic_client.alter_topic(
52+
path=args.topic_path,
53+
add_consumers=[args.topic_consumer],
54+
)
55+
self.logger.info("Consumer added successfully: %s", args.topic_consumer)
4956
return
5057
else:
51-
self.logger.info("Consumer already exists: %s", args.consumer)
58+
self.logger.info("Consumer already exists: %s", args.topic_consumer)
5259
return
5360

5461
except Exception as alter_err:
@@ -68,6 +75,65 @@ def create(self, args):
6875

6976
raise RuntimeError("Failed to create topic")
7077

78+
async def create_async(self, args):
79+
assert self.driver is not None, "Driver is not initialized. Call set_driver() before create_async()."
80+
retry_no = 0
81+
while retry_no < 3:
82+
self.logger.info("Creating topic: %s (retry no: %d)", args.topic_path, retry_no)
83+
84+
try:
85+
await self.driver.topic_client.create_topic(
86+
path=args.topic_path,
87+
min_active_partitions=args.topic_partitions,
88+
max_active_partitions=args.topic_partitions,
89+
consumers=[args.topic_consumer],
90+
)
91+
92+
self.logger.info("Topic created successfully: %s", args.topic_path)
93+
self.logger.info("Consumer created: %s", args.topic_consumer)
94+
return
95+
96+
except ydb.Error as e:
97+
error_msg = str(e).lower()
98+
if "already exists" in error_msg:
99+
self.logger.info("Topic already exists: %s", args.topic_path)
100+
101+
try:
102+
description = await self.driver.topic_client.describe_topic(args.topic_path)
103+
consumer_exists = any(c.name == args.topic_consumer for c in description.consumers)
104+
105+
if not consumer_exists:
106+
self.logger.info(
107+
"Adding consumer %s to existing topic",
108+
args.topic_consumer,
109+
)
110+
await self.driver.topic_client.alter_topic(
111+
path=args.topic_path,
112+
add_consumers=[args.topic_consumer],
113+
)
114+
self.logger.info("Consumer added successfully: %s", args.topic_consumer)
115+
return
116+
else:
117+
self.logger.info("Consumer already exists: %s", args.topic_consumer)
118+
return
119+
120+
except Exception as alter_err:
121+
self.logger.warning("Failed to add consumer: %s", alter_err)
122+
raise
123+
elif "storage pool" in error_msg or "pq" in error_msg:
124+
self.logger.error("YDB instance does not support topics (PersistentQueues): %s", e)
125+
self.logger.error("Please use YDB instance with topic support")
126+
raise
127+
elif isinstance(e, ydb.Unavailable):
128+
self.logger.info("YDB instance is not ready, retrying in 5 seconds...")
129+
await asyncio.sleep(5)
130+
retry_no += 1
131+
else:
132+
self.logger.error("Failed to create topic: %s", e)
133+
raise
134+
135+
raise RuntimeError("Failed to create topic")
136+
71137
def run(self, args):
72138
assert self.driver is not None, "Driver is not initialized. Call set_driver() before run()."
73139
metrics = create_metrics(args)
@@ -99,15 +165,29 @@ async def run_async(self, args):
99165
metrics.reset()
100166

101167
def cleanup(self, args):
102-
self.logger.info("Cleaning up topic: %s", args.path)
168+
self.logger.info("Cleaning up topic: %s", args.topic_path)
103169

104170
assert self.driver is not None, "Driver is not initialized. Call set_driver() before cleanup()."
105171
try:
106-
self.driver.topic_client.drop_topic(args.path)
107-
self.logger.info("Topic dropped: %s", args.path)
172+
self.driver.topic_client.drop_topic(args.topic_path)
173+
self.logger.info("Topic dropped: %s", args.topic_path)
174+
except ydb.Error as e:
175+
if "does not exist" in str(e).lower():
176+
self.logger.info("Topic does not exist: %s", args.topic_path)
177+
else:
178+
self.logger.error("Failed to drop topic: %s", e)
179+
raise
180+
181+
async def cleanup_async(self, args):
182+
self.logger.info("Cleaning up topic: %s", args.topic_path)
183+
184+
assert self.driver is not None, "Driver is not initialized. Call set_driver() before cleanup_async()."
185+
try:
186+
await self.driver.topic_client.drop_topic(args.topic_path)
187+
self.logger.info("Topic dropped: %s", args.topic_path)
108188
except ydb.Error as e:
109189
if "does not exist" in str(e).lower():
110-
self.logger.info("Topic does not exist: %s", args.path)
190+
self.logger.info("Topic does not exist: %s", args.topic_path)
111191
else:
112192
self.logger.error("Failed to drop topic: %s", e)
113193
raise

0 commit comments

Comments
 (0)