This is an automated email from the ASF dual-hosted git repository.
anton-vinogradov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 0d65ca707c1 IGNITE-28932 [ducktests] Make garbage collector selectable
via globals (#13413)
0d65ca707c1 is described below
commit 0d65ca707c1dc217be4d9e3570cebee5f844d34a
Author: Maksim Davydov <[email protected]>
AuthorDate: Fri Sep 11 18:01:22 2026 +0300
IGNITE-28932 [ducktests] Make garbage collector selectable via globals
(#13413)
---
modules/ducktests/README.md | 49 ++++++
.../tests/checks/utils/check_gc_params.py | 142 +++++++++++++++
.../tests/checks/utils/check_ignite_spec.py | 190 ++++++++++++++++++++-
.../tests/checks/utils/check_jvm_settings.py | 64 ++++++-
.../ducktests/tests/docker/requirements-dev.txt | 3 +-
modules/ducktests/tests/docker/requirements.txt | 2 +-
.../utils/cdc/ignite_to_kafka_cdc_helper.py | 9 +-
.../services/utils/cdc/kafka_to_ignite.py | 6 +-
.../tests/ignitetest/services/utils/gc_params.py | 88 ++++++++++
.../ignitetest/services/utils/ignite_aware.py | 13 +-
.../tests/ignitetest/services/utils/ignite_spec.py | 79 +++++++--
.../tests/ignitetest/services/utils/jvm_utils.py | 128 ++++++++++++--
.../ignitetest/services/utils/metrics/metrics.py | 4 +-
modules/ducktests/tests/tox.ini | 2 +-
14 files changed, 741 insertions(+), 38 deletions(-)
diff --git a/modules/ducktests/README.md b/modules/ducktests/README.md
index b52c22c8a02..4f0ccc3dde1 100644
--- a/modules/ducktests/README.md
+++ b/modules/ducktests/README.md
@@ -185,6 +185,7 @@ You can modify test environments at execution time using
global flags injected t
| Global Parameter Key | Definition | Example Configuration |
|---------------------|------------|----------------------|
+| **gc** | Garbage collector to run nodes with, selectable independently for
the `server` and `client` roles. Default is `G1` for both. See [Garbage
Collector Selection](#garbage-collector-selection) below. | ```{"gc":
{"server": "ZGC", "client": "SERIAL"}}``` |
| **jfr_enabled** | Boolean flag to enable Java Flight Recorder for
performance profiling. Default is False. | ```{"jfr_enabled": true}``` |
| **safepoint_log_enabled** | Boolean flag to enable safepoint logging for
debugging JVM behavior. Default is False. | ```{"safepoint_log_enabled":
true}``` |
| **jmx_remote** | JMX remote monitoring configuration with nested parameters.
Enabled flag controls remote JMX access, port specifies the listening port
(default is 1098). | ```{"jmx_remote": {"enabled": true, "port": 1099}}``` |
@@ -229,6 +230,51 @@ You can target specific cross-product version
compatibility combinations inside
```
+### Garbage Collector Selection
+
+The collector is chosen with the `gc` global. It is *mutually-exclusive group
replacement*: a collector and the tuning flags that are meaningful for it
travel together, so selecting one swaps the whole group. This is why it cannot
be done by passing `-XX:+UseZGC` in `jvm_opts` — that leaves two selectors on
the command line and the JVM aborts at startup with "Multiple garbage
collectors selected".
+
+Three shapes are accepted:
+
+```bash
+# Both roles
+--global-json '{"gc": "ZGC"}'
+
+# Servers only; clients keep the default
+--global-json '{"gc": {"server": "ZGC"}}'
+
+# Per role
+--global-json '{"gc": {"server": "ZGC", "client": "SERIAL"}}'
+
+# Raw JVM options -- escape hatch, bypasses the registry and its validation
+--global-json '{"gc": {"server": ["-XX:+UseZGC", "-XX:SoftMaxHeapSize=2G"]}}'
+```
+
+Profile names are case-insensitive (`"zgc"` == `"ZGC"`). An unknown name fails
immediately with the list of valid names rather than silently falling back.
+
+| Profile | Options set |
+|---------|-------------|
+| **G1** (default) | `-XX:+UseG1GC`, `-XX:MaxGCPauseMillis=100`,
`-XX:ConcGCThreads`, `-XX:ParallelGCThreads`, `-XX:+UseStringDeduplication` |
+| **PARALLEL** | `-XX:+UseParallelGC`, `-XX:ParallelGCThreads` — deliberately
no `MaxGCPauseMillis`, which would flip ParallelGC into adaptive pause-goal
sizing |
+| **SERIAL** | `-XX:+UseSerialGC` |
+| **ZGC** | `-XX:+UseZGC`, `-XX:ConcGCThreads`, `-XX:ParallelGCThreads` |
+| **SHENANDOAH** | `-XX:+UseShenandoahGC`, `-XX:ConcGCThreads`,
`-XX:ParallelGCThreads` — OpenJDK builds only, absent from Oracle JDK |
+
+`-XX:+UseStringDeduplication` is part of the G1 profile because it is G1-only
through JDK 17; it is not applied under any other collector.
+
+**Roles** are determined semantically, not by service class: a service is a
`server` only if it is a full Ignite node that is not in client mode.
Client-mode `IgniteService`s, application services, thin clients, thin JDBC and
the Kafka-CDC `kafka-to-ignite` utility all resolve as `client`.
+
+**Precedence**, lowest to highest:
+
+```
+G1 default < gc global for the role < jvm_opts passed by the test
+```
+
+Caveats:
+* A service constructed with `merge_with_default=False` drops every default,
so the `gc` global does not reach it — its collector comes from `jvm_opts`
alone. A warning is logged when this happens.
+* The raw-list form is used verbatim and is *not* validated, so two selectors
in one list still reach the JVM.
+* Conflicting selectors from any other route raise a Python exception when the
service is constructed, rather than failing later in a remote JVM.
+
### Diagnostics & Performance Utilities
```bash
# Enable Java Flight Recorder (JFR) tracing
@@ -236,6 +282,9 @@ You can target specific cross-product version compatibility
combinations inside
# Enable JVM Safepoints performance logging
--global-json '{"safepoint_log_enabled": true}'
+
+# Run servers under ZGC and clients under SerialGC
+--global-json '{"gc": {"server": "ZGC", "client": "SERIAL"}}'
```
### Demo Mode (Breakpoints)
diff --git a/modules/ducktests/tests/checks/utils/check_gc_params.py
b/modules/ducktests/tests/checks/utils/check_gc_params.py
new file mode 100644
index 00000000000..d83d9fee40d
--- /dev/null
+++ b/modules/ducktests/tests/checks/utils/check_gc_params.py
@@ -0,0 +1,142 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Checks resolution of the 'gc' global into garbage collector options.
+"""
+
+import pytest
+
+from ignitetest.services.utils.gc_params import CLIENT_ROLE, SERVER_ROLE,
resolve_gc_settings
+from ignitetest.services.utils.jvm_utils import GC_PROFILES, GC_G1, GC_SERIAL,
GC_Z
+
+
+class CheckGcParams:
+ """
+ Checks the 'gc' global parser.
+ """
+
+ @pytest.mark.parametrize('role', [SERVER_ROLE, CLIENT_ROLE])
+ def check_default__when_gc_global_absent(self, role):
+ """
+ The no-op path: without the global both roles get G1.
+ """
+ assert resolve_gc_settings({}, role) == GC_PROFILES[GC_G1]
+ assert resolve_gc_settings({"cluster_size": 3}, role) ==
GC_PROFILES[GC_G1]
+ assert resolve_gc_settings(None, role) == GC_PROFILES[GC_G1]
+
+ @pytest.mark.parametrize('role', [SERVER_ROLE, CLIENT_ROLE])
+ def check_bare_string__applies_to_both_roles(self, role):
+ """
+ Bare string is sugar for {"server": X, "client": X}.
+ """
+ assert resolve_gc_settings({"gc": "ZGC"}, role) == GC_PROFILES[GC_Z]
+
+ def check_per_role_mapping(self):
+ """
+ Each role resolves independently.
+ """
+ _globals = {"gc": {"server": "ZGC", "client": "SERIAL"}}
+
+ assert resolve_gc_settings(_globals, SERVER_ROLE) == GC_PROFILES[GC_Z]
+ assert resolve_gc_settings(_globals, CLIENT_ROLE) ==
GC_PROFILES[GC_SERIAL]
+
+ def check_omitted_role__falls_back_to_default(self):
+ """
+ A role missing from the mapping keeps the default collector.
+ """
+ _globals = {"gc": {"server": "ZGC"}}
+
+ assert resolve_gc_settings(_globals, SERVER_ROLE) == GC_PROFILES[GC_Z]
+ assert resolve_gc_settings(_globals, CLIENT_ROLE) == GC_PROFILES[GC_G1]
+
+ @pytest.mark.parametrize('name', ["zgc", "ZGC", "Zgc", "shenandoah",
"Parallel"])
+ def check_profile_names__are_case_insensitive(self, name):
+ """
+ Profile names are matched case-insensitively.
+ """
+ assert resolve_gc_settings({"gc": name}, SERVER_ROLE) ==
GC_PROFILES[name.upper()]
+
+ def check_raw_list__is_passed_through_verbatim(self):
+ """
+ A list bypasses the registry entirely -- the documented escape hatch.
+ """
+ raw = ["-XX:+UseZGC", "-XX:SoftMaxHeapSize=2G"]
+
+ assert resolve_gc_settings({"gc": raw}, SERVER_ROLE) == raw
+ assert resolve_gc_settings({"gc": {"server": raw}}, SERVER_ROLE) == raw
+ assert resolve_gc_settings({"gc": {"server": raw}}, CLIENT_ROLE) ==
GC_PROFILES[GC_G1]
+
+ def check_raw_list__is_not_aliased_to_the_globals(self):
+ """
+ The caller must not be able to mutate the globals through the returned
list.
+ """
+ raw = ["-XX:+UseZGC"]
+
+ resolved = resolve_gc_settings({"gc": raw}, SERVER_ROLE)
+ resolved.append("-XX:SoftMaxHeapSize=2G")
+
+ assert raw == ["-XX:+UseZGC"]
+
+ def check_profile__is_not_aliased_to_the_registry(self):
+ """
+ Mutating a resolved profile must not corrupt the registry for the next
service.
+ """
+ resolved = resolve_gc_settings({"gc": "SERIAL"}, SERVER_ROLE)
+ resolved.append("-XX:SoftMaxHeapSize=2G")
+
+ assert resolve_gc_settings({"gc": "SERIAL"}, SERVER_ROLE) ==
GC_PROFILES[GC_SERIAL]
+
+ def check_unknown_profile__raises_and_lists_valid_names(self):
+ """
+ An unknown name fails loudly instead of silently falling back to G1.
+ """
+ with pytest.raises(ValueError) as err:
+ resolve_gc_settings({"gc": "CMS"}, SERVER_ROLE)
+
+ for name in GC_PROFILES:
+ assert name in str(err.value)
+
+ def check_unexpected_value__raises(self):
+ """
+ Neither a profile name, nor raw options, nor a role mapping.
+ """
+ with pytest.raises(ValueError):
+ resolve_gc_settings({"gc": 42}, SERVER_ROLE)
+
+ with pytest.raises(ValueError):
+ resolve_gc_settings({"gc": {"server": 42}}, SERVER_ROLE)
+
+ @pytest.mark.parametrize('name', [name for name in GC_PROFILES if name !=
GC_G1])
+ def check_non_g1_profiles__carry_no_g1_only_flags(self, name):
+ """
+ MaxGCPauseMillis flips ParallelGC into adaptive pause-goal sizing, and
UseStringDeduplication is
+ G1-only until JDK 18. Neither may leak into another collector's
profile.
+ """
+ resolved = resolve_gc_settings({"gc": name}, SERVER_ROLE)
+
+ assert not any("MaxGCPauseMillis" in opt for opt in resolved)
+ assert not any("UseStringDeduplication" in opt for opt in resolved)
+
+ @pytest.mark.parametrize('name', list(GC_PROFILES))
+ def check_every_profile__selects_exactly_one_collector(self, name):
+ """
+ A profile is a mutually exclusive group -- exactly one selector, no
spaces or quotes (the options
+ are interpolated into a remote shell command).
+ """
+ resolved = resolve_gc_settings({"gc": name}, SERVER_ROLE)
+
+ assert len([opt for opt in resolved if opt.startswith("-XX:+Use") and
opt.endswith("GC")]) == 1
+ assert not any(" " in opt or "'" in opt or '"' in opt for opt in
resolved)
diff --git a/modules/ducktests/tests/checks/utils/check_ignite_spec.py
b/modules/ducktests/tests/checks/utils/check_ignite_spec.py
index b60fce333e1..1e2cc4d3920 100644
--- a/modules/ducktests/tests/checks/utils/check_ignite_spec.py
+++ b/modules/ducktests/tests/checks/utils/check_ignite_spec.py
@@ -21,12 +21,14 @@ from unittest.mock import Mock
import pytest
-from ignitetest.services.utils.ignite_spec import IgniteApplicationSpec
+from ignitetest.services.utils import IgniteServiceType
+from ignitetest.services.utils.gc_params import CLIENT_ROLE, SERVER_ROLE
+from ignitetest.services.utils.ignite_spec import IgniteApplicationSpec,
IgniteNodeSpec, service_role
+from ignitetest.services.utils.jvm_utils import GC_PROFILES, GC_G1,
MultipleGcSelectedError
from ignitetest.utils.ignite_test import JFR_ENABLED
[email protected]
-def service():
+def mock_service(service_type=IgniteServiceType.NODE, client_mode=False):
"""
Create mock of service.
"""
@@ -35,10 +37,28 @@ def service():
service.persistent_root = ''
service.context.globals = {"cluster_size": 1}
service.log_config_file = ''
+ service.config.service_type = service_type
+ service.config.client_mode = client_mode
return service
[email protected]
+def service():
+ """
+ Mock of a server node service.
+ """
+ return mock_service()
+
+
[email protected]
+def client_service():
+ """
+ Mock of a client node service.
+ """
+ return mock_service(client_mode=True)
+
+
"""
Checks that the JVM options passed via constructor are not overriden by the
default ones.
"""
@@ -103,3 +123,167 @@ def
check_colon_options__go_after_default_ones_and_overwrite_them__if_passed_via
assert "-Xlog:gc:/some-non-default-path/gc.log" in spec.jvm_opts
assert default_gc_log in spec.jvm_opts
assert spec.jvm_opts.index("-Xlog:gc:/some-non-default-path/gc.log") >
spec.jvm_opts.index(default_gc_log)
+
+
+"""
+Checks GC selection via the 'gc' global.
+"""
+
+
[email protected](
+ 'service_type,client_mode,expected',
+ [
+ [IgniteServiceType.NODE, False, SERVER_ROLE],
+ [IgniteServiceType.NODE, True, CLIENT_ROLE], # IgniteService can
run in client mode
+ [IgniteServiceType.THIN_CLIENT, None, CLIENT_ROLE], # client_mode
absent on thin configs
+ [IgniteServiceType.THIN_JDBC, None, CLIENT_ROLE],
+ [IgniteServiceType.NONE, None, CLIENT_ROLE],
+ ]
+)
+def check_service_role__is_determined_semantically(service_type, client_mode,
expected):
+ assert service_role(mock_service(service_type, client_mode)) == expected
+
+
+def check_g1__is_used__if_gc_global_is_absent(service):
+ spec = IgniteNodeSpec(service)
+ for opt in GC_PROFILES[GC_G1]:
+ assert opt in spec.jvm_opts
+
+
+def check_gc_global__applies_to_both_roles__if_passed_as_bare_string(service,
client_service):
+ service.context.globals["gc"] = "ZGC"
+ client_service.context.globals["gc"] = "ZGC"
+
+ for spec in (IgniteNodeSpec(service), IgniteNodeSpec(client_service)):
+ assert "-XX:+UseZGC" in spec.jvm_opts
+ assert "-XX:+UseG1GC" not in spec.jvm_opts
+
+
+def check_gc_global__applies_per_role__if_passed_as_mapping(service,
client_service):
+ gc_global = {"server": "ZGC", "client": "SERIAL"}
+ service.context.globals["gc"] = gc_global
+ client_service.context.globals["gc"] = gc_global
+
+ server_spec = IgniteNodeSpec(service)
+ client_spec = IgniteNodeSpec(client_service)
+
+ assert "-XX:+UseZGC" in server_spec.jvm_opts
+ assert "-XX:+UseSerialGC" in client_spec.jvm_opts
+
+ for spec in (server_spec, client_spec):
+ assert "-XX:+UseG1GC" not in spec.jvm_opts
+ assert "-XX:+UseStringDeduplication" not in spec.jvm_opts
+ assert not any("MaxGCPauseMillis" in opt for opt in spec.jvm_opts)
+
+
+def
check_string_deduplication__is_not_applied__under_non_g1_collectors(service):
+ assert "-XX:+UseStringDeduplication" in IgniteNodeSpec(service).jvm_opts
+
+ service.context.globals["gc"] = "PARALLEL"
+ assert "-XX:+UseStringDeduplication" not in
IgniteNodeSpec(service).jvm_opts
+
+
+def
check_conflicting_gc_selectors__raise__if_gc_is_also_passed_via_jvm_opts(service):
+ service.context.globals["gc"] = "ZGC"
+
+ with pytest.raises(MultipleGcSelectedError) as err:
+ IgniteNodeSpec(service, jvm_opts=["-XX:+UseSerialGC"])
+
+ assert "-XX:+UseZGC" in str(err.value)
+ assert "-XX:+UseSerialGC" in str(err.value)
+
+
+def
check_conflicting_gc_selectors__raise__if_both_are_passed_via_jvm_opts(service):
+ with pytest.raises(MultipleGcSelectedError):
+ IgniteNodeSpec(service, jvm_opts=["-XX:+UseSerialGC",
"-XX:+UseParallelGC"], merge_with_default=False)
+
+
+def check_disabled_gc_selector__does_not_conflict(service):
+ """
+ -XX:-UseG1GC turns G1 off, so the collector picked afterwards is the only
one enabled.
+ """
+ spec = IgniteNodeSpec(service, jvm_opts=["-XX:-UseG1GC",
"-XX:+UseSerialGC"])
+
+ assert "-XX:+UseSerialGC" in spec.jvm_opts
+
+
+def check_gc_global__does_not_apply__if_merge_with_default_is_false(service):
+ service.context.globals["gc"] = "ZGC"
+
+ spec = IgniteNodeSpec(service, jvm_opts="-XX:+UseSerialGC",
merge_with_default=False)
+
+ assert spec.jvm_opts == ["-XX:+UseSerialGC"]
+ service.logger.warning.assert_called_once()
+
+
+"""
+Checks that a spec keeps the caller's delta separate from its own resolution,
so another service can
+inherit the delta without inheriting role-dependent options. See the CDC path.
+"""
+
+
+def check_user_jvm_opts__holds_the_delta__not_the_resolution(service):
+ spec = IgniteNodeSpec(service, jvm_opts=["-Xmx8G", "-DFOO=bar"])
+
+ assert spec.user_jvm_opts == ["-Xmx8G", "-DFOO=bar"]
+ assert spec.merge_with_default is True
+
+ assert "-Xmx8G" in spec.jvm_opts
+ assert "-XX:+UseG1GC" in spec.jvm_opts # resolution is richer than the
delta
+
+
+def check_user_jvm_opts__splits_a_string_delta(service):
+ assert IgniteNodeSpec(service, jvm_opts="-Xmx8G -ea").user_jvm_opts ==
["-Xmx8G", "-ea"]
+
+
[email protected]('merge_with_default', [True, False])
+def check_rebuild_as__reproduces_the_original_resolution(service,
merge_with_default):
+ spec = IgniteNodeSpec(service, jvm_opts=["-Xmx8G"],
merge_with_default=merge_with_default)
+
+ assert spec.rebuild_as(IgniteNodeSpec).jvm_opts == spec.jvm_opts
+
+
+def check_rebuild_as__does_not_leak_gc_across_roles(client_service):
+ """
+ The CDC regression: a client-role service inheriting a server cluster's
delta must keep its own
+ collector, and must end up with exactly one selector.
+ """
+ server = mock_service()
+ server.context.globals["gc"] = {"server": "ZGC", "client": "SERIAL"}
+ client_service.context.globals["gc"] = {"server": "ZGC", "client":
"SERIAL"}
+
+ server_spec = IgniteNodeSpec(server, jvm_opts=["-Xmx8G"])
+
+ # What ignite_to_kafka_cdc_helper does: hand the destination cluster's
delta to a client service.
+ inherited = IgniteNodeSpec(client_service,
jvm_opts=server_spec.user_jvm_opts)
+
+ assert "-XX:+UseZGC" in server_spec.jvm_opts
+ assert "-XX:+UseSerialGC" in inherited.jvm_opts
+ assert "-XX:+UseZGC" not in inherited.jvm_opts
+ assert "-Xmx8G" in inherited.jvm_opts # the delta itself is still
inherited
+
+ for spec in (server_spec, inherited):
+ assert len([opt for opt in spec.jvm_opts if opt.startswith("-XX:+Use")
and opt.endswith("GC")]) == 1
+
+
+"""
+Checks assembly of spec-specific defaults.
+"""
+
+
+def check_application_spec__has_matched_heap_bounds(service):
+ spec = IgniteApplicationSpec(service)
+
+ assert "-Xmx1G" in spec.jvm_opts
+ assert "-Xms1G" in spec.jvm_opts
+ assert len([opt for opt in spec.jvm_opts if opt.startswith("-Xmx")]) == 1
+ assert len([opt for opt in spec.jvm_opts if opt.startswith("-Xms")]) == 1
+
+
+def check_heap_override__from_jvm_opts__still_wins(service):
+ spec = IgniteApplicationSpec(service, jvm_opts=["-Xmx4G", "-Xms4G"])
+
+ assert "-Xmx4G" in spec.jvm_opts
+ assert "-Xms4G" in spec.jvm_opts
+ assert "-Xmx1G" not in spec.jvm_opts
+ assert "-Xms1G" not in spec.jvm_opts
diff --git a/modules/ducktests/tests/checks/utils/check_jvm_settings.py
b/modules/ducktests/tests/checks/utils/check_jvm_settings.py
index 0fb44bef9dd..b15000d6687 100644
--- a/modules/ducktests/tests/checks/utils/check_jvm_settings.py
+++ b/modules/ducktests/tests/checks/utils/check_jvm_settings.py
@@ -19,7 +19,8 @@ Checks JVM settings.
import pytest
-from ignitetest.services.utils.jvm_utils import create_jvm_settings,
merge_jvm_settings, DEFAULT_HEAP
+from ignitetest.services.utils.jvm_utils import create_jvm_settings,
merge_jvm_settings, validate_gc_settings, \
+ DEFAULT_HEAP, GC_PROFILES, GC_G1, GC_SERIAL, MultipleGcSelectedError
class CheckJVMSettings:
@@ -75,3 +76,64 @@ class CheckJVMSettings:
res[param] = 1
assert res == expected
+
+ def check_default_gc(self):
+ """
+ Without an explicit collector, create_jvm_settings yields the default
profile and nothing from
+ any other one.
+ """
+ jvm_settings = create_jvm_settings()
+
+ for opt in GC_PROFILES[GC_G1]:
+ assert opt in jvm_settings
+
+ assert "-XX:+UseStringDeduplication" not in
create_jvm_settings(gc_settings=GC_PROFILES[GC_SERIAL])
+
+ @pytest.mark.parametrize('gc_settings', [GC_PROFILES[GC_SERIAL],
"-XX:+UseSerialGC"])
+ def check_gc_settings_accepts_list_and_string(self, gc_settings):
+ """
+ A stray caller passing a string keeps working.
+ """
+ assert "-XX:+UseSerialGC" in
create_jvm_settings(gc_settings=gc_settings)
+
+ @pytest.mark.parametrize('gc_settings', [{"server": "ZGC"}, 42,
("-XX:+UseSerialGC",)])
+ def check_gc_settings_rejects_other_types(self, gc_settings):
+ """
+ Anything else fails at the boundary: a dict would silently join to its
keys.
+ """
+ with pytest.raises(AssertionError):
+ create_jvm_settings(gc_settings=gc_settings)
+
+ @pytest.mark.parametrize(
+ 'jvm_opts',
+ [
+ ["-XX:+UseG1GC", "-XX:+UseZGC"],
+ ["-XX:+UseSerialGC", "-XX:+UseParallelGC", "-XX:+UseZGC"],
+ "-XX:+UseG1GC -XX:+UseShenandoahGC",
+ ]
+ )
+ def check_multiple_gc_selectors_raise(self, jvm_opts):
+ """
+ Two enabled collectors abort the JVM at startup; catch it in Python
instead.
+ """
+ with pytest.raises(MultipleGcSelectedError):
+ validate_gc_settings(jvm_opts)
+
+ with pytest.raises(MultipleGcSelectedError):
+ merge_jvm_settings([], jvm_opts)
+
+ @pytest.mark.parametrize(
+ 'jvm_opts',
+ [
+ ["-XX:+UseG1GC"],
+ ["-XX:+UseG1GC", "-XX:-UseZGC"],
+ ["-XX:+UseG1GC", "-XX:-UseG1GC", "-XX:+UseZGC"], # last
occurrence per collector wins
+ # neither of these is a collector selector, despite matching on a
naive pattern
+ ["-XX:+UseG1GC", "-XX:+DisableExplicitGC",
"-XX:+UseStringDeduplication"],
+ ]
+ )
+ def check_single_gc_selector_passes(self, jvm_opts):
+ """
+ One enabled collector, however it was arrived at, is fine.
+ """
+ assert validate_gc_settings(jvm_opts) == jvm_opts
diff --git a/modules/ducktests/tests/docker/requirements-dev.txt
b/modules/ducktests/tests/docker/requirements-dev.txt
index 8ce0758663a..6f3ee95a485 100644
--- a/modules/ducktests/tests/docker/requirements-dev.txt
+++ b/modules/ducktests/tests/docker/requirements-dev.txt
@@ -16,4 +16,5 @@
-r requirements.txt
pytest==6.2.5
flake8==6.1.0
-tox
+tox==4.25.0
+virtualenv==20.35.4
diff --git a/modules/ducktests/tests/docker/requirements.txt
b/modules/ducktests/tests/docker/requirements.txt
index 1fc5cac411e..efb316f7dd5 100644
--- a/modules/ducktests/tests/docker/requirements.txt
+++ b/modules/ducktests/tests/docker/requirements.txt
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-filelock==3.8.2
+filelock==3.16.1
ducktape==0.15.0
looseversion==1.3.0
tcconfig==0.29.1
diff --git
a/modules/ducktests/tests/ignitetest/services/utils/cdc/ignite_to_kafka_cdc_helper.py
b/modules/ducktests/tests/ignitetest/services/utils/cdc/ignite_to_kafka_cdc_helper.py
index 5aaf1314d8d..0f1834cdc3c 100644
---
a/modules/ducktests/tests/ignitetest/services/utils/cdc/ignite_to_kafka_cdc_helper.py
+++
b/modules/ducktests/tests/ignitetest/services/utils/cdc/ignite_to_kafka_cdc_helper.py
@@ -46,8 +46,11 @@ class IgniteToKafkaCdcHelper(CdcHelper):
cdc_params.kafka,
dst_cluster,
cdc_params=cdc_params,
- jvm_opts=dst_cluster.spec.jvm_opts,
- merge_with_default=True,
+ # The delta the test applied to the destination cluster, not its
resolved option list --
+ # dst_cluster is a server and kafka_to_ignite is a client, so
copying the resolution would
+ # carry the server's collector into a client JVM.
+ jvm_opts=dst_cluster.spec.user_jvm_opts,
+ merge_with_default=dst_cluster.spec.merge_with_default,
modules=dst_cluster.modules
)
@@ -128,7 +131,7 @@ def get_ignite_to_kafka_spec(base, kafka_connection_string,
service):
return templates
- return IgniteToKafkaSpec(service, service.spec.jvm_opts,
merge_with_default=True)
+ return service.spec.rebuild_as(IgniteToKafkaSpec)
class KafkaCdcParams(CdcParams):
diff --git
a/modules/ducktests/tests/ignitetest/services/utils/cdc/kafka_to_ignite.py
b/modules/ducktests/tests/ignitetest/services/utils/cdc/kafka_to_ignite.py
index e5cba2a0b83..9516476beb5 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/cdc/kafka_to_ignite.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/cdc/kafka_to_ignite.py
@@ -25,6 +25,7 @@ from ignitetest.services.utils import IgniteServiceType
from ignitetest.services.utils.config_template import ConfigTemplate
from ignitetest.services.utils.ignite_configuration import
IgniteThinClientConfiguration
from ignitetest.services.utils.ignite_spec import envs_to_exports
+from ignitetest.services.utils.jvm_utils import merge_jvm_settings
from ignitetest.utils.bean import Bean
@@ -57,7 +58,8 @@ class KafkaToIgniteService(IgniteService):
self.spec = get_kafka_to_ignite_spec(self.spec.__class__,
kafka.connection_string(), self)
- self.spec.jvm_opts += ["-Dlog4j.configurationFile=file:" +
self.log_config_file]
+ self.spec.jvm_opts = merge_jvm_settings(
+ self.spec.jvm_opts, ["-Dlog4j.configurationFile=file:" +
self.log_config_file])
self.kafka = kafka
@@ -219,7 +221,7 @@ def get_kafka_to_ignite_spec(base, kafka_connection_string,
service):
else:
return self.service.script(cmd)
- return KafkaToIgniteSpec(service, service.spec.jvm_opts)
+ return service.spec.rebuild_as(KafkaToIgniteSpec)
class KafkaPropertiesTemplate(ConfigTemplate):
diff --git a/modules/ducktests/tests/ignitetest/services/utils/gc_params.py
b/modules/ducktests/tests/ignitetest/services/utils/gc_params.py
new file mode 100644
index 00000000000..91e03927c84
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/gc_params.py
@@ -0,0 +1,88 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+"""
+This module resolves the garbage collector to use from Globals.
+
+GC selection is mutually-exclusive group replacement: a collector and its
tuning flags travel together
+(see GC_PROFILES in jvm_utils). It therefore has to be chosen *before* the
default option list is
+assembled -- patching it afterwards via jvm_opts leaves two selectors in the
command line, because
+merge_jvm_settings overwrites per option, not per group.
+
+This is the single resolution point for the 'gc' global. Keep it that way.
+"""
+
+from ignitetest.services.utils.jvm_utils import DEFAULT_GC, GC_PROFILES
+
+GC_KEY_NAME = "gc"
+
+SERVER_ROLE = "server"
+CLIENT_ROLE = "client"
+
+
+def is_gc_configured(_globals: dict):
+ """
+ :param _globals: Globals parameters
+ :return: True if the run explicitly selects a garbage collector.
+ """
+ return GC_KEY_NAME in (_globals or {})
+
+
+def resolve_gc_settings(_globals: dict, role: str):
+ """
+ Gets garbage collector options from Globals. Three shapes are accepted:
+
+ {"gc": "ZGC"} -- both roles
+ {"gc": {"server": "ZGC"}} -- servers only,
clients keep the default
+ {"gc": {"server": "ZGC", "client": "SERIAL"}} -- per role
+ {"gc": {"server": ["-XX:+UseZGC", "-XX:SoftMaxHeapSize=2G"]}} -- raw
options, escape hatch
+
+ Profile names are case-insensitive. A missing role, or a missing 'gc' key,
yields the DEFAULT_GC
+ profile. A list value is used verbatim and bypasses profile validation --
that is the point of it.
+
+ :param _globals: Globals parameters
+ :param role: SERVER_ROLE or CLIENT_ROLE
+ :return: list of JVM options selecting and tuning the collector
+ """
+ configured = (_globals or {}).get(GC_KEY_NAME)
+
+ if configured is None:
+ return _profile(DEFAULT_GC)
+
+ if isinstance(configured, dict):
+ configured = configured.get(role)
+
+ if configured is None:
+ return _profile(DEFAULT_GC)
+
+ if isinstance(configured, list):
+ return list(configured)
+
+ if isinstance(configured, str):
+ name = configured.upper()
+
+ if name not in GC_PROFILES:
+ raise ValueError(f"Unknown garbage collector profile
'{configured}' for role '{role}'. "
+ f"Valid profiles: {',
'.join(sorted(GC_PROFILES))}. "
+ f"A list of raw JVM options is also accepted.")
+
+ return _profile(name)
+
+ raise ValueError(f"Unexpected value for the '{GC_KEY_NAME}' global:
{configured!r}. Expected a profile "
+ f"name, a list of raw JVM options, or a mapping of role
to either of those.")
+
+
+def _profile(name):
+ return list(GC_PROFILES[name])
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
index 8bd758f339d..3e148217945 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
@@ -117,10 +117,21 @@ class IgniteAwareService(BackgroundThreadService,
IgnitePathAware, JvmProcessMix
super().start_node(node, **kwargs)
- wait_until(lambda: self.alive(node), timeout_sec=10)
+ wait_until(lambda: self.alive(node), timeout_sec=10, err_msg=lambda:
self.__jvm_startup_failure_msg(node))
ignite_jmx_mixin(node, self)
+ def __jvm_startup_failure_msg(self, node):
+ """
+ A JVM that rejects an option (an unknown collector, a bad heap value,
two collectors selected)
+ dies before it logs anything Ignite-shaped, so without the console
tail this is a bare timeout.
+ """
+ console_log = os.path.join(self.log_dir, "console.log")
+
+ output = node.account.ssh_capture(f"tail -n 30 {console_log}",
allow_fail=True)
+
+ return f"{self.who_am_i(node)}: JVM did not start within 10 seconds.
Tail of {console_log}:\n{output}"
+
def stop_async(self, force_stop=False, **kwargs):
"""
Stop in async way.
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
index 1edad489ee3..ae7036df48c 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
@@ -30,7 +30,8 @@ from itertools import chain
from ignitetest.services.utils import IgniteServiceType
from ignitetest.services.utils.config_template import
IgniteClientConfigTemplate, IgniteServerConfigTemplate, \
IgniteLoggerConfigTemplate, IgniteThinClientConfigTemplate,
IgniteThinJdbcConfigTemplate
-from ignitetest.services.utils.jvm_utils import create_jvm_settings,
merge_jvm_settings
+from ignitetest.services.utils.gc_params import is_gc_configured,
resolve_gc_settings, CLIENT_ROLE, SERVER_ROLE
+from ignitetest.services.utils.jvm_utils import create_jvm_settings,
merge_jvm_settings, validate_gc_settings
from ignitetest.services.utils.path import get_home_dir, IgnitePathAware
from ignitetest.services.utils.ssl.ssl_params import is_ssl_enabled
from ignitetest.services.utils.metrics.metrics import
is_opencensus_metrics_enabled, configure_opencensus_metrics, \
@@ -77,6 +78,24 @@ def envs_to_exports(envs):
return "; ".join(exports) + ";"
+def service_role(service):
+ """
+ Determine the GC role of a service semantically rather than by spec class:
IgniteService can run in
+ client mode, and subclasses of it (KafkaToIgniteService) are clients too,
so an isinstance check
+ would hand those nodes server settings.
+
+ :param service: Service
+ :return: SERVER_ROLE or CLIENT_ROLE
+ """
+ config = service.config
+
+ # client_mode is absent on thin-client / thin-JDBC configs, so the service
type check must come first.
+ if config.service_type == IgniteServiceType.NODE and not
config.client_mode:
+ return SERVER_ROLE
+
+ return CLIENT_ROLE
+
+
class IgniteSpec(metaclass=ABCMeta):
"""
This class is a basic Spec
@@ -91,14 +110,51 @@ class IgniteSpec(metaclass=ABCMeta):
default options will be applied.
"""
self.service = service
- self.jvm_opts = merge_jvm_settings(self.__get_default_jvm_opts() if
merge_with_default else [],
- jvm_opts if jvm_opts else [])
+
+ # The caller's delta is kept alongside the merged result so that
another service can inherit the
+ # tuning a test applied here without inheriting this service's
role-dependent options too.
+ self.user_jvm_opts = jvm_opts.split() if isinstance(jvm_opts, str)
else list(jvm_opts or [])
+ self.merge_with_default = merge_with_default
+
+ if merge_with_default:
+ defaults = merge_jvm_settings(self.__get_default_jvm_opts(),
self._service_defaults())
+ else:
+ defaults = []
+
+ if is_gc_configured(self.service.context.globals):
+ self.service.logger.warning(
+ f"{type(self.service).__name__} is built with
merge_with_default=False, so the 'gc' global "
+ f"does not apply to it. Its collector comes from jvm_opts
only.")
+
+ self.jvm_opts = merge_jvm_settings(defaults, self.user_jvm_opts)
+
+ def rebuild_as(self, spec_class, **kwargs):
+ """
+ Re-create this spec as another class, reproducing the original
resolution.
+
+ Passing the *resolved* self.jvm_opts instead would leak this service's
role-dependent options
+ (the GC block) into the new spec.
+
+ :param spec_class: IgniteSpec subclass to build.
+ :return: instance of spec_class.
+ """
+ return spec_class(self.service, jvm_opts=self.user_jvm_opts,
+ merge_with_default=self.merge_with_default, **kwargs)
+
+ def _service_defaults(self):
+ """
+ :return: spec-specific default JVM options, merged after the common
defaults.
+ """
+ return []
def __get_default_jvm_opts(self):
"""
Return a set of default JVM options.
"""
- default_jvm_opts =
create_jvm_settings(gc_dump_path=os.path.join(self.service.log_dir, "gc.log"),
+ gc_settings = resolve_gc_settings(self.service.context.globals,
service_role(self.service))
+
+ default_jvm_opts = create_jvm_settings(gc_settings=gc_settings,
+
gc_dump_path=os.path.join(self.service.log_dir, "gc.log"),
oom_path=os.path.join(self.service.log_dir, "out_of_mem.hprof"),
vm_error_path=os.path.join(self.service.log_dir, "hs_err_pid%p.log"))
@@ -310,6 +366,10 @@ class IgniteSpec(metaclass=ABCMeta):
"""
:return: line with extra JVM params for ignite.sh script:
-J-Dparam=value -J-ea
"""
+ # Second validation pass: every code path building a command line goes
through here, so this
+ # catches options appended to jvm_opts directly, bypassing
merge_jvm_settings.
+ validate_gc_settings(self.jvm_opts)
+
opts = ["-J%s" % o for o in self.jvm_opts]
return " ".join(opts)
@@ -342,17 +402,12 @@ class IgniteApplicationSpec(IgniteSpec):
"""
Spec to run ignite application
"""
- def __init__(self, service, jvm_opts=None, merge_with_default=True):
- super().__init__(
- service,
- merge_jvm_settings(self.__get_default_jvm_opts() if
merge_with_default else [],
- jvm_opts if jvm_opts else []),
- merge_with_default)
-
- def __get_default_jvm_opts(self):
+ def _service_defaults(self):
return [
"-DIGNITE_NO_SHUTDOWN_HOOK=true", # allows performing operations
on app termination.
"-Xmx1G",
+ "-Xms1G",
+ "-XX:+AlwaysPreTouch",
"-ea",
"-DIGNITE_ALLOW_ATOMIC_OPS_IN_TX=false"
]
diff --git a/modules/ducktests/tests/ignitetest/services/utils/jvm_utils.py
b/modules/ducktests/tests/ignitetest/services/utils/jvm_utils.py
index b67c0a471bc..29b1ee831b0 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/jvm_utils.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/jvm_utils.py
@@ -17,25 +17,83 @@
This module contains JVM utilities.
"""
+import re
+
from ignitetest.services.utils.decorators import memoize
DEFAULT_HEAP = "768M"
-JVM_PARAMS_GC_G1 = "-XX:+UseG1GC -XX:MaxGCPauseMillis=100 " \
- "-XX:ConcGCThreads=$(((`nproc`/3)>1?(`nproc`/3):1)) " \
- "-XX:ParallelGCThreads=$(((`nproc`*3/4)>1?(`nproc`*3/4):1))
"
+GC_G1 = "G1"
+GC_PARALLEL = "PARALLEL"
+GC_SERIAL = "SERIAL"
+GC_Z = "ZGC"
+GC_SHENANDOAH = "SHENANDOAH"
+
+DEFAULT_GC = GC_G1
+
+# NOTE: these strings are interpolated into a shell command that is evaluated
on the remote
+# node (see IgniteSpec._jvm_opts and IgniteNodeSpec.command), which is what
makes the `nproc`
+# substitutions work. Consequently NO option here may contain spaces or quotes.
+_NPROC_THIRD = "$(((`nproc`/3)>1?(`nproc`/3):1))"
+_NPROC_THREE_QUARTERS = "$(((`nproc`*3/4)>1?(`nproc`*3/4):1))"
+
+# Garbage collector profiles. A profile is a mutually exclusive group: it both
selects the collector
+# and carries the tuning flags that are meaningful for it. Never mix flags
across profiles.
+GC_PROFILES = {
+ GC_G1: [
+ "-XX:+UseG1GC",
+ "-XX:MaxGCPauseMillis=100",
+ f"-XX:ConcGCThreads={_NPROC_THIRD}",
+ f"-XX:ParallelGCThreads={_NPROC_THREE_QUARTERS}",
+ "-XX:+UseStringDeduplication", # G1-only until JDK 18, hence part of
the profile
+ ],
+ GC_PARALLEL: [
+ "-XX:+UseParallelGC",
+ f"-XX:ParallelGCThreads={_NPROC_THREE_QUARTERS}",
+ # deliberately NO MaxGCPauseMillis: it flips ParallelGC into adaptive
pause-goal sizing
+ ],
+ GC_SERIAL: [
+ "-XX:+UseSerialGC",
+ ],
+ GC_Z: [
+ "-XX:+UseZGC", # product feature since JDK 15, no unlock flag needed
+ f"-XX:ConcGCThreads={_NPROC_THIRD}",
+ f"-XX:ParallelGCThreads={_NPROC_THREE_QUARTERS}",
+ ],
+ GC_SHENANDOAH: [
+ "-XX:+UseShenandoahGC", # product feature since JDK 15; OpenJDK only,
not Oracle JDK
+ f"-XX:ConcGCThreads={_NPROC_THIRD}",
+ f"-XX:ParallelGCThreads={_NPROC_THREE_QUARTERS}",
+ ],
+}
JVM_PARAMS_GENERIC = "-server -XX:+DisableExplicitGC -XX:+AlwaysPreTouch " \
"-XX:+ParallelRefProcEnabled -XX:+DoEscapeAnalysis " \
- "-XX:+OptimizeStringConcat -XX:+UseStringDeduplication"
+ "-XX:+OptimizeStringConcat"
+
+# Matches a collector selector like -XX:+UseZGC. Deliberately narrow: it must
not match
+# -XX:+DisableExplicitGC or -XX:+UseStringDeduplication.
+_GC_SELECTOR_PATTERN = re.compile(r"^-XX:([+-])(Use\w+GC)$")
-def create_jvm_settings(heap_size=DEFAULT_HEAP, gc_settings=JVM_PARAMS_GC_G1,
generic_params=JVM_PARAMS_GENERIC,
+class MultipleGcSelectedError(Exception):
+ """
+ Raised when JVM options end up selecting more than one garbage collector.
+ """
+
+
+def create_jvm_settings(heap_size=DEFAULT_HEAP, gc_settings=None,
generic_params=JVM_PARAMS_GENERIC,
gc_dump_path=None, oom_path=None, vm_error_path=None):
"""
Provides settings string for JVM process.
- param opts: JVM options to merge. Adds new or rewrites default values. Can
be list or string.
+ :param heap_size: value for both -Xmx and -Xms.
+ :param gc_settings: garbage collector options, see GC_PROFILES. Can be
list or string.
+ Defaults to the DEFAULT_GC profile.
+ :param generic_params: collector-independent options. Can be list or
string.
"""
+ gc_settings = _as_opts_list(GC_PROFILES[DEFAULT_GC] if gc_settings is None
else gc_settings, "gc_settings")
+ generic_params = _as_opts_list(generic_params, "generic_params")
+
gc_dump = ""
if gc_dump_path:
gc_dump = "-Xlog:gc*=debug,gc+stats*=debug,gc+ergo*=debug:" +
gc_dump_path + ":uptime,time,level,tags"
@@ -48,8 +106,8 @@ def create_jvm_settings(heap_size=DEFAULT_HEAP,
gc_settings=JVM_PARAMS_GC_G1, ge
if vm_error_path:
vm_error_dump = "-XX:ErrorFile=" + vm_error_path
- as_string = f"-Xmx{heap_size} -Xms{heap_size} {gc_settings} {gc_dump} " \
- f"{out_of_mem_dump} {vm_error_dump} {generic_params}".strip()
+ as_string = f"-Xmx{heap_size} -Xms{heap_size} {' '.join(gc_settings)}
{gc_dump} " \
+ f"{out_of_mem_dump} {vm_error_dump} {'
'.join(generic_params)}".strip()
return as_string.split()
@@ -74,9 +132,45 @@ def merge_jvm_settings(src_settings, additionals):
else:
listed.append(param)
+ validate_gc_settings(listed)
+
return listed
+def validate_gc_settings(jvm_opts):
+ """
+ Checks that at most one garbage collector is selected.
+
+ GC selection is a mutually exclusive group, but merge_jvm_settings is a
per-option overwrite model
+ keyed on the substring before the first '=' -- so -XX:+UseG1GC and
-XX:+UseZGC are different keys and
+ both survive a merge. The resulting JVM aborts at startup with "Multiple
garbage collectors selected",
+ which surfaces on the Python side as an unexplained node startup timeout.
Fail here instead.
+
+ :param jvm_opts: JVM options to check. Can be list or string.
+ :raise MultipleGcSelectedError: if more than one collector is enabled.
+ """
+ jvm_opts = _as_opts_list(jvm_opts, "JVM options")
+
+ # Last occurrence wins, matching how the JVM itself resolves repeated
flags.
+ selectors = {}
+
+ for opt in jvm_opts:
+ match = _GC_SELECTOR_PATTERN.match(opt)
+ if match:
+ selectors[match.group(2)] = (match.group(1) == "+", opt)
+
+ enabled = sorted(opt for is_enabled, opt in selectors.values() if
is_enabled)
+
+ if len(enabled) > 1:
+ raise MultipleGcSelectedError(
+ f"Multiple garbage collectors selected: {', '.join(enabled)}. "
+ f"Select a collector with the 'gc' global instead of passing it
via jvm_opts, "
+ f"e.g. --global-json '{{\"gc\": \"ZGC\"}}'. "
+ f"Valid profiles: {', '.join(sorted(GC_PROFILES))}.")
+
+ return jvm_opts
+
+
def java_major_version(version):
"""
:param version: Full java version
@@ -102,12 +196,22 @@ def java_version(node):
return raw_version[0].strip() if raw_version else ''
+def _as_opts_list(params, name="JVM params"):
+ """
+ Normalizes JVM options to a list: a string is split on whitespace, a list
is copied.
+
+ :param params: options as a string or a list.
+ :param name: what to call them in the failure message.
+ :return: options as a list.
+ """
+ assert isinstance(params, (str, list)), f"{name} can be string or list
only, got {type(params).__name__}."
+
+ return params.split() if isinstance(params, str) else list(params)
+
+
def _to_map(params):
""""""
- assert isinstance(params, (str, list)), "JVM params an be string or list
only."
-
- if isinstance(params, str):
- params = params.split()
+ params = _as_opts_list(params)
mapped = {}
diff --git
a/modules/ducktests/tests/ignitetest/services/utils/metrics/metrics.py
b/modules/ducktests/tests/ignitetest/services/utils/metrics/metrics.py
index d40713f3609..cbb39666aea 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/metrics/metrics.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/metrics/metrics.py
@@ -15,6 +15,7 @@
from typing import NamedTuple
+from ignitetest.services.utils.jvm_utils import merge_jvm_settings
from ignitetest.utils.bean import Bean
from ignitetest.utils.version import V_2_7_6
@@ -73,7 +74,8 @@ def configure_opencensus_metrics(config, _globals, spec):
sendInstanceName=True))
if not any("opencensus.metrics.port" in jvm_opt for jvm_opt in
spec.jvm_opts):
- spec.jvm_opts.append("-Dopencensus.metrics.port=%d" %
metrics_params.port)
+ spec.jvm_opts = merge_jvm_settings(spec.jvm_opts,
+ ["-Dopencensus.metrics.port=%d" %
metrics_params.port])
if not any(bean[0] == OPENCENSUS_TEMPLATE_FILE for bean in
config.ext_beans):
config.ext_beans.append((OPENCENSUS_TEMPLATE_FILE, metrics_params))
diff --git a/modules/ducktests/tests/tox.ini b/modules/ducktests/tests/tox.ini
index c40e563eb26..efb728f84a4 100644
--- a/modules/ducktests/tests/tox.ini
+++ b/modules/ducktests/tests/tox.ini
@@ -14,7 +14,6 @@
# limitations under the License.
[tox]
envlist = codestyle, py{38,39,310,311,312,313}
-skipsdist = True
[testenv]
usedevelop = True
@@ -31,6 +30,7 @@ commands = pytest {env:PYTESTARGS:} {posargs}
[testenv:codestyle]
basepython = python3
+skip_install = True
deps = flake8
commands = flake8