>();
JobVertexID jobVertexID = new JobVertexID();
@@ -145,7 +148,7 @@ CompletableFuture sendRequest(
@Test
@Timeout(60)
- public void testJmMetricCollection() throws Exception {
+ void testJmMetricCollection() throws Exception {
try (MiniCluster miniCluster =
new MiniCluster(
new MiniClusterConfiguration.Builder()
@@ -160,36 +163,38 @@ public void testJmMetricCollection() throws Exception {
(c, e) ->
new StandaloneClientHAServices(
miniCluster.getRestAddress().get().toString()));
- do {
- var collector = new RestApiMetricsCollector<>();
- Map flinkMetricMetricMap =
- collector.queryJmMetrics(
- client,
- Map.of(
- "taskSlotsTotal", FlinkMetric.NUM_TASK_SLOTS_TOTAL,
- "taskSlotsAvailable",
- FlinkMetric.NUM_TASK_SLOTS_AVAILABLE));
- try {
- assertEquals(
- "3",
- flinkMetricMetricMap.get(FlinkMetric.NUM_TASK_SLOTS_TOTAL).getValue());
- assertEquals(
- "3",
- flinkMetricMetricMap
- .get(FlinkMetric.NUM_TASK_SLOTS_AVAILABLE)
- .getValue());
- break;
- } catch (NullPointerException e) {
- // Metrics might not be available yet (timeout above will eventually kill this
- // test)
- Thread.sleep(100);
- }
- } while (true);
+ var collector = new RestApiMetricsCollector<>();
+ Map flinkMetricMetricMap = new HashMap<>();
+ // Metrics might not be available yet so retry the query until it returns results or the
+ // timeout reached.
+ await().atMost(Duration.ofSeconds(60))
+ .until(
+ () -> {
+ final Map results =
+ collector.queryJmMetrics(
+ client,
+ Map.of(
+ "taskSlotsTotal",
+ FlinkMetric.NUM_TASK_SLOTS_TOTAL,
+ "taskSlotsAvailable",
+ FlinkMetric.NUM_TASK_SLOTS_AVAILABLE));
+ flinkMetricMetricMap.putAll(results);
+ return !results.isEmpty();
+ });
+
+ assertThat(flinkMetricMetricMap)
+ .hasSize(2)
+ .hasEntrySatisfying(
+ FlinkMetric.NUM_TASK_SLOTS_TOTAL,
+ metricValue -> assertMetricValueIs(metricValue, 3))
+ .hasEntrySatisfying(
+ FlinkMetric.NUM_TASK_SLOTS_AVAILABLE,
+ metricValue -> assertMetricValueIs(metricValue, 3));
}
}
@Test
- public void testTmMetricCollection() throws Exception {
+ void testTmMetricCollection() throws Exception {
var metricValues = new HashMap();
@@ -309,4 +314,8 @@ private static void assertMetricsEquals(
assertEquals(v.getSum(), a.getSum(), k.name());
});
}
+
+ private static void assertMetricValueIs(Metric metricValue, int expected) {
+ assertThat(metricValue.getValue()).asInt().isEqualTo(expected);
+ }
}
diff --git a/flink-kubernetes-operator-api/pom.xml b/flink-kubernetes-operator-api/pom.xml
index 40ec264245..81160bfd85 100644
--- a/flink-kubernetes-operator-api/pom.xml
+++ b/flink-kubernetes-operator-api/pom.xml
@@ -32,7 +32,6 @@ under the License.
jar
- 4.1.0
${project.build.directory}/plugins
diff --git a/flink-kubernetes-operator/pom.xml b/flink-kubernetes-operator/pom.xml
index 75909e43c0..6010d6bc67 100644
--- a/flink-kubernetes-operator/pom.xml
+++ b/flink-kubernetes-operator/pom.xml
@@ -32,7 +32,6 @@ under the License.
jar
- 4.1.0
${project.build.directory}/plugins
diff --git a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/metrics/KubernetesClientMetricsTest.java b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/metrics/KubernetesClientMetricsTest.java
index 88b85d3dd3..53b8aaaa90 100644
--- a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/metrics/KubernetesClientMetricsTest.java
+++ b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/metrics/KubernetesClientMetricsTest.java
@@ -24,6 +24,8 @@
import org.apache.flink.kubernetes.operator.config.FlinkOperatorConfiguration;
import org.apache.flink.kubernetes.operator.utils.KubernetesClientUtils;
import org.apache.flink.metrics.Counter;
+import org.apache.flink.metrics.Histogram;
+import org.apache.flink.metrics.Meter;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
@@ -32,6 +34,7 @@
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.awaitility.Awaitility;
import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -39,6 +42,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
import static org.apache.flink.kubernetes.operator.metrics.KubernetesClientMetrics.COUNTER;
import static org.apache.flink.kubernetes.operator.metrics.KubernetesClientMetrics.HISTO;
@@ -48,16 +52,16 @@
import static org.apache.flink.kubernetes.operator.metrics.KubernetesClientMetrics.KUBE_CLIENT_GROUP;
import static org.apache.flink.kubernetes.operator.metrics.KubernetesClientMetrics.METER;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.byLessThan;
import static org.assertj.core.api.InstanceOfAssertFactories.LONG;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
/** {@link KubernetesClientMetrics} tests. */
@EnableKubernetesMockClient(crud = true)
@TestMethodOrder(OrderAnnotation.class)
-public class KubernetesClientMetricsTest {
+class KubernetesClientMetricsTest {
private KubernetesMockServer mockServer;
private static final String REQUEST_COUNTER_ID =
@@ -97,9 +101,14 @@ public class KubernetesClientMetricsTest {
private static final String RESPONSE_LATENCY_ID =
String.join(".", KUBE_CLIENT_GROUP, HTTP_RESPONSE_GROUP, HISTO);
+ @BeforeAll
+ static void beforeAll() {
+ Awaitility.ignoreExceptionByDefault(AssertionError.class);
+ }
+
@Test
@Order(1)
- public void testMetricsDisabled() {
+ void testMetricsDisabled() {
var configuration = new Configuration();
configuration.set(
KubernetesOperatorMetricOptions.OPERATOR_KUBERNETES_CLIENT_METRICS_ENABLED, false);
@@ -125,7 +134,7 @@ public void testMetricsDisabled() {
@Test
@Order(2)
- public void testMetricsEnabled() {
+ void testMetricsEnabled() {
var configuration = new Configuration();
var listener = new TestingMetricListener(configuration);
var kubernetesClient =
@@ -135,106 +144,53 @@ public void testMetricsEnabled() {
mockServer.createClient().getConfiguration());
var deployment = TestUtils.buildApplicationCluster();
- assertEquals(
- 0, listener.getCounter(listener.getMetricId(REQUEST_COUNTER_ID)).get().getCount());
- assertEquals(
- 0.0, listener.getMeter(listener.getMetricId(REQUEST_METER_ID)).get().getRate());
- assertEquals(
- 0,
- listener.getCounter(listener.getMetricId(REQUEST_FAILED_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 0.0,
- listener.getMeter(listener.getMetricId(REQUEST_FAILED_METER_ID)).get().getRate());
- assertEquals(
- 0, listener.getCounter(listener.getMetricId(RESPONSE_COUNTER_ID)).get().getCount());
- assertEquals(
- 0.0, listener.getMeter(listener.getMetricId(RESPONSE_METER_ID)).get().getRate());
- assertEquals(
- 0,
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMin());
- assertEquals(
- 0,
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMax());
+ assertCounterIsZero(listener, REQUEST_COUNTER_ID);
+ assertRateIsZero(listener, REQUEST_METER_ID);
+ assertCounterIsZero(listener, REQUEST_FAILED_COUNTER_ID);
+ assertRateIsZero(listener, REQUEST_FAILED_METER_ID);
+ assertCounterIsZero(listener, RESPONSE_COUNTER_ID);
+ assertRateIsZero(listener, RESPONSE_METER_ID);
+ assertHistogramHasZeroStatistics(listener);
kubernetesClient.resource(deployment).createOrReplace();
- assertEquals(
- 1, listener.getCounter(listener.getMetricId(REQUEST_COUNTER_ID)).get().getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(REQUEST_POST_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1, listener.getCounter(listener.getMetricId(RESPONSE_COUNTER_ID)).get().getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_201_COUNTER_ID))
- .get()
- .getCount());
- assertTrue(
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMin()
- > 0);
- assertTrue(
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMax()
- > 0);
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertCounterHasValue(listener, REQUEST_COUNTER_ID, 1);
+ assertCounterHasValue(listener, REQUEST_POST_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_201_COUNTER_ID, 1);
+
+ assertHistogramHasStatistics(listener);
+ return true;
+ });
kubernetesClient.resource(deployment).delete();
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(REQUEST_DELETE_COUNTER_ID))
- .get()
- .getCount());
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertCounterHasValue(listener, REQUEST_DELETE_COUNTER_ID, 1);
+ return true;
+ });
kubernetesClient.resource(deployment).delete();
- assertEquals(
- 2,
- listener.getCounter(listener.getMetricId(REQUEST_DELETE_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_404_COUNTER_ID))
- .get()
- .getCount());
- Awaitility.await()
- .atMost(1, TimeUnit.MINUTES)
+ await().atMost(20, TimeUnit.SECONDS)
.until(
() -> {
- kubernetesClient.resource(deployment).createOrReplace();
- return listener.getMeter(listener.getMetricId(REQUEST_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(listener.getMetricId(RESPONSE_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_201_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_404_METER_ID))
- .get()
- .getRate()
- > 0.01;
+ assertCounterHasValue(listener, REQUEST_DELETE_COUNTER_ID, 2);
+ assertCounterHasValue(listener, RESPONSE_404_COUNTER_ID, 1);
+ return true;
+ });
+
+ kubernetesClient.resource(deployment).createOrReplace();
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertPositiveRate(listener, REQUEST_METER_ID);
+ assertPositiveRate(listener, RESPONSE_METER_ID);
+ assertPositiveRate(listener, RESPONSE_201_METER_ID);
+ assertPositiveRate(listener, RESPONSE_404_METER_ID);
+ return true;
});
}
@@ -285,8 +241,7 @@ public void onDelete(
})) {
// Then
- Awaitility.await()
- .atMost(1, TimeUnit.MINUTES)
+ await().atMost(20, TimeUnit.SECONDS)
.untilAtomic(watchEventCount, Matchers.greaterThanOrEqualTo(1L));
assertThat(requestCounter)
.extracting(Counter::getCount)
@@ -304,7 +259,7 @@ public void onDelete(
@Test
@Order(3)
- public void testMetricsHttpResponseCodeGroupsEnabled() {
+ void testMetricsHttpResponseCodeGroupsEnabled() {
var configuration = new Configuration();
configuration.set(
KubernetesOperatorMetricOptions
@@ -318,134 +273,63 @@ public void testMetricsHttpResponseCodeGroupsEnabled() {
mockServer.createClient().getConfiguration());
var deployment = TestUtils.buildApplicationCluster();
- assertEquals(
- 0, listener.getCounter(listener.getMetricId(REQUEST_COUNTER_ID)).get().getCount());
- assertEquals(
- 0.0, listener.getMeter(listener.getMetricId(REQUEST_METER_ID)).get().getRate());
- assertEquals(
- 0,
- listener.getCounter(listener.getMetricId(REQUEST_FAILED_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 0.0,
- listener.getMeter(listener.getMetricId(REQUEST_FAILED_METER_ID)).get().getRate());
- assertEquals(
- 0, listener.getCounter(listener.getMetricId(RESPONSE_COUNTER_ID)).get().getCount());
- assertEquals(
- 0.0, listener.getMeter(listener.getMetricId(RESPONSE_METER_ID)).get().getRate());
- assertEquals(
- 0,
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMin());
- assertEquals(
- 0,
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMax());
+
+ assertCounterIsZero(listener, REQUEST_COUNTER_ID);
+ assertRateIsZero(listener, REQUEST_METER_ID);
+ assertCounterIsZero(listener, REQUEST_FAILED_COUNTER_ID);
+ assertRateIsZero(listener, REQUEST_FAILED_METER_ID);
+ assertCounterIsZero(listener, RESPONSE_COUNTER_ID);
+ assertRateIsZero(listener, RESPONSE_METER_ID);
+ assertHistogramHasZeroStatistics(listener);
kubernetesClient.resource(deployment).createOrReplace();
- assertEquals(
- 1, listener.getCounter(listener.getMetricId(REQUEST_COUNTER_ID)).get().getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(REQUEST_POST_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1, listener.getCounter(listener.getMetricId(RESPONSE_COUNTER_ID)).get().getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_201_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_2xx_COUNTER_ID))
- .get()
- .getCount());
- assertTrue(
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMin()
- > 0);
- assertTrue(
- listener.getHistogram(listener.getMetricId(RESPONSE_LATENCY_ID))
- .get()
- .getStatistics()
- .getMax()
- > 0);
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertCounterHasValue(listener, REQUEST_COUNTER_ID, 1);
+ assertCounterHasValue(listener, REQUEST_POST_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_201_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_2xx_COUNTER_ID, 1);
+ assertHistogramHasStatistics(listener);
+ return true;
+ });
kubernetesClient.resource(deployment).delete();
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(REQUEST_DELETE_COUNTER_ID))
- .get()
- .getCount());
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertCounterHasValue(listener, REQUEST_DELETE_COUNTER_ID, 1);
+ return true;
+ });
kubernetesClient.resource(deployment).delete();
- assertEquals(
- 2,
- listener.getCounter(listener.getMetricId(REQUEST_DELETE_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_404_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 1,
- listener.getCounter(listener.getMetricId(RESPONSE_4xx_COUNTER_ID))
- .get()
- .getCount());
- Awaitility.await()
- .atMost(1, TimeUnit.MINUTES)
+ await().atMost(20, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ assertCounterHasValue(listener, REQUEST_DELETE_COUNTER_ID, 2);
+ assertCounterHasValue(listener, RESPONSE_404_COUNTER_ID, 1);
+ assertCounterHasValue(listener, RESPONSE_404_COUNTER_ID, 1);
+ return true;
+ });
+
+ await().atMost(20, TimeUnit.SECONDS)
.until(
() -> {
kubernetesClient.resource(deployment).createOrReplace();
- return listener.getMeter(listener.getMetricId(REQUEST_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(listener.getMetricId(RESPONSE_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_201_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_404_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_2xx_METER_ID))
- .get()
- .getRate()
- > 0.01
- && listener.getMeter(
- listener.getMetricId(
- RESPONSE_4xx_METER_ID))
- .get()
- .getRate()
- > 0.01;
+ assertPositiveRate(listener, REQUEST_METER_ID);
+ assertPositiveRate(listener, RESPONSE_METER_ID);
+ assertPositiveRate(listener, RESPONSE_201_METER_ID);
+ assertPositiveRate(listener, RESPONSE_404_METER_ID);
+ assertPositiveRate(listener, RESPONSE_2xx_METER_ID);
+ assertPositiveRate(listener, RESPONSE_4xx_METER_ID);
+ return true;
});
}
@Test
@Order(3)
- public void testAPIServerIsDown() {
+ void testAPIServerIsDown() {
var configuration = new Configuration();
var listener = new TestingMetricListener(configuration);
var kubernetesClient =
@@ -456,33 +340,104 @@ public void testAPIServerIsDown() {
var deployment = TestUtils.buildApplicationCluster();
mockServer.shutdown();
- assertEquals(
- 0,
- listener.getCounter(listener.getMetricId(REQUEST_FAILED_COUNTER_ID))
- .get()
- .getCount());
- assertEquals(
- 0.0,
- listener.getMeter(listener.getMetricId(REQUEST_FAILED_METER_ID)).get().getRate());
- Awaitility.await()
- .atMost(1, TimeUnit.MINUTES)
+
+ assertCounterIsZero(listener, REQUEST_FAILED_COUNTER_ID);
+ assertRateIsZero(listener, REQUEST_FAILED_METER_ID);
+
+ await().atMost(20, TimeUnit.SECONDS)
.until(
() -> {
assertThrows(
KubernetesClientException.class,
() -> kubernetesClient.resource(deployment).createOrReplace());
- return listener.getCounter(
- listener.getMetricId(
- REQUEST_FAILED_COUNTER_ID))
- .get()
- .getCount()
- > 0
- && listener.getMeter(
- listener.getMetricId(
- REQUEST_FAILED_METER_ID))
- .get()
- .getRate()
- > 0.01;
+ assertCounterIsPositive(listener);
+ assertPositiveRate(listener, REQUEST_FAILED_METER_ID);
+ return true;
});
}
+
+ private static void assertRateIsZero(TestingMetricListener listener, String meterId) {
+ assertRate(
+ listener,
+ meterId,
+ meter -> assertThat(meter.getRate()).isCloseTo(0.0, byLessThan(0.00001)));
+ }
+
+ private static void assertPositiveRate(TestingMetricListener listener, String meterId) {
+ assertRate(
+ listener,
+ meterId,
+ meter -> assertThat(meter.getRate()).isGreaterThanOrEqualTo(0.01));
+ }
+
+ private static void assertRate(
+ TestingMetricListener listener, String meterId, Consumer meterConsumer) {
+ assertThat(listener.getMeter(listener.getMetricId(meterId)))
+ .hasValueSatisfying(meterConsumer);
+ }
+
+ private static void assertCounterIsPositive(TestingMetricListener listener) {
+ assertCounterHasValue(
+ listener,
+ KubernetesClientMetricsTest.REQUEST_FAILED_COUNTER_ID,
+ counter -> assertThat(counter.getCount()).isPositive());
+ }
+
+ private static void assertCounterIsZero(TestingMetricListener listener, String counterId) {
+ assertCounterHasValue(
+ listener, counterId, counter -> assertThat(counter.getCount()).isZero());
+ }
+
+ private static void assertCounterHasValue(
+ TestingMetricListener listener, String requestDeleteCounterId, int expected) {
+ assertCounterHasValue(
+ listener,
+ requestDeleteCounterId,
+ counter -> assertThat(counter.getCount()).isEqualTo(expected));
+ }
+
+ private static void assertCounterHasValue(
+ TestingMetricListener listener,
+ String requestDeleteCounterId,
+ Consumer counterConsumer) {
+ assertThat(listener.getCounter(listener.getMetricId(requestDeleteCounterId)))
+ .hasValueSatisfying(counterConsumer);
+ }
+
+ private static void assertHistogramHasStatistics(TestingMetricListener listener) {
+ assertHistogramStatistics(
+ listener,
+ histogram -> {
+ assertThat(histogram.getCount()).isPositive();
+ assertThat(histogram.getStatistics())
+ .satisfies(
+ stats -> {
+ assertThat(stats.getMin()).isPositive();
+ assertThat(stats.getMax()).isPositive();
+ });
+ });
+ }
+
+ private static void assertHistogramHasZeroStatistics(TestingMetricListener listener) {
+ assertHistogramStatistics(
+ listener,
+ histogram -> {
+ assertThat(histogram.getCount()).isZero();
+ assertThat(histogram.getStatistics())
+ .satisfies(
+ stats -> {
+ assertThat(stats.getMin()).isZero();
+ assertThat(stats.getMax()).isZero();
+ });
+ });
+ }
+
+ private static void assertHistogramStatistics(
+ TestingMetricListener listener, Consumer histogramConsumer) {
+ assertThat(
+ listener.getHistogram(
+ listener.getMetricId(
+ KubernetesClientMetricsTest.RESPONSE_LATENCY_ID)))
+ .hasValueSatisfying(histogramConsumer);
+ }
}
diff --git a/pom.xml b/pom.xml
index 8d5ea274e6..92f45a00c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,6 +110,7 @@ under the License.
10.15.2.0
+ 4.2.2