Support clustered endpoint for Schema Registry (#1483)
* Add failover support for Schema Registry * Base schema id on primary node * Made code thread safe * Remove unnecessary synchronize * Remove duplicated url field with InternalSchemaRegistry * Fix maven warnings about dynamic versioning (#1559) * Bump @types/react-redux from 7.1.18 to 7.1.22 in /kafka-ui-react-app (#1462) Bumps [@types/react-redux](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-redux) from 7.1.18 to 7.1.22. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-redux) --- updated-dependencies: - dependency-name: "@types/react-redux" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @types/jest from 27.0.3 to 27.4.0 in /kafka-ui-react-app (#1458) Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 27.0.3 to 27.4.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) --- updated-dependencies: - dependency-name: "@types/jest" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Console banner updated (#1319) * banner changed to fix new name * width adjusted to 80 to fit all terminals Co-authored-by: iliax <ikuramshin@provectus.com> Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com> * Add an example for SSL w/ kafka (#1568) Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com> Co-authored-by: Ruslan Ibragimov <ruibragimov@provectus.com> * Smart filters: Groovy script messages filter implementation (reopened) (#1547) * groovy script messages filter added * ISSUE-943: Topic messages tailing implementation (#1515) * Topic messages tailing implementation * Implemented topics sorting by size (#1539) Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com> * [ISSUE-1512]Added sorting by topics size * [ISSUE-1512]Added sorting by topics size * Add sort by Size.Refactoring sort order * correct a little mistake * Improve test coverage * got rid code dupliction * refactoring Co-authored-by: ValentinPrischepa <valentin.prischepa@gmail.com> Co-authored-by: Anton Zorin <ant.zorin@gmail.com> Co-authored-by: Oleg Shur <workshur@gmail.com> * Implement recreating a topic * [ISSUE-998][backend] Add functionality to re-create topic in one click * [ISSUE-998][backend] Add functionality to re-create topic in one click * [ISSUE-998][backend] Add functionality to re-create topic in one click Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com> * Run the app in the container as a non-root user (#1575) * Run as a non-root user. Fixes #1555 Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com> * Fix line break Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com> Co-authored-by: Ruslan Ibragimov <94184844+5hin0bi@users.noreply.github.com> * [FIXED issue/1545] added feedback to the user when a message content is copied to clipboard (#1570) * added alert after "Copy to clipborad" * moved main logic to useDataSaver * fixed typographical mistake * updated useDataSaver test * made adaptive heigth in connectors config component (#1583) Co-authored-by: Anton Zorin <zorii4@Antons-MacBook-Pro.local> * Bump http-proxy-middleware from 2.0.1 to 2.0.3 in /kafka-ui-react-app (#1579) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.1 to 2.0.3. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/master/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.1...v2.0.3) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Allow running sonar within PR of a fork (#1581) * Allow running sonar within PR of a fork * remove github token from envs on mvn verify * Wrap failover in Mono.as * Use failoverable uri instead of primary node one for accessing the schemaregistry * Added urls to similarly named configureWebClient methods Co-authored-by: Jonas Geiregat (31198) <jonas.geiregat@tvh.com>
This commit is contained in:
parent
ea9a145583
commit
07a7836773
7 changed files with 281 additions and 61 deletions
|
@ -12,6 +12,7 @@ import com.provectus.kafka.ui.model.CompatibilityLevelDTO;
|
||||||
import com.provectus.kafka.ui.model.ConfigSourceDTO;
|
import com.provectus.kafka.ui.model.ConfigSourceDTO;
|
||||||
import com.provectus.kafka.ui.model.ConfigSynonymDTO;
|
import com.provectus.kafka.ui.model.ConfigSynonymDTO;
|
||||||
import com.provectus.kafka.ui.model.ConnectDTO;
|
import com.provectus.kafka.ui.model.ConnectDTO;
|
||||||
|
import com.provectus.kafka.ui.model.FailoverUrlList;
|
||||||
import com.provectus.kafka.ui.model.Feature;
|
import com.provectus.kafka.ui.model.Feature;
|
||||||
import com.provectus.kafka.ui.model.InternalBrokerConfig;
|
import com.provectus.kafka.ui.model.InternalBrokerConfig;
|
||||||
import com.provectus.kafka.ui.model.InternalBrokerDiskUsage;
|
import com.provectus.kafka.ui.model.InternalBrokerDiskUsage;
|
||||||
|
@ -97,8 +98,8 @@ public interface ClusterMapper {
|
||||||
|
|
||||||
internalSchemaRegistry.url(
|
internalSchemaRegistry.url(
|
||||||
clusterProperties.getSchemaRegistry() != null
|
clusterProperties.getSchemaRegistry() != null
|
||||||
? Arrays.asList(clusterProperties.getSchemaRegistry().split(","))
|
? new FailoverUrlList(Arrays.asList(clusterProperties.getSchemaRegistry().split(",")))
|
||||||
: Collections.emptyList()
|
: new FailoverUrlList(Collections.emptyList())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (clusterProperties.getSchemaRegistryAuth() != null) {
|
if (clusterProperties.getSchemaRegistryAuth() != null) {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.provectus.kafka.ui.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import lombok.experimental.Delegate;
|
||||||
|
|
||||||
|
public class FailoverUrlList {
|
||||||
|
|
||||||
|
public static final int DEFAULT_RETRY_GRACE_PERIOD_IN_MS = 5000;
|
||||||
|
|
||||||
|
private final Map<Integer, Instant> failures = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicInteger index = new AtomicInteger(0);
|
||||||
|
@Delegate
|
||||||
|
private final List<String> urls;
|
||||||
|
private final int retryGracePeriodInMs;
|
||||||
|
|
||||||
|
public FailoverUrlList(List<String> urls) {
|
||||||
|
this(urls, DEFAULT_RETRY_GRACE_PERIOD_IN_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FailoverUrlList(List<String> urls, int retryGracePeriodInMs) {
|
||||||
|
if (urls != null && !urls.isEmpty()) {
|
||||||
|
this.urls = new ArrayList<>(urls);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Expected at least one URL to be passed in constructor");
|
||||||
|
}
|
||||||
|
this.retryGracePeriodInMs = retryGracePeriodInMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String current() {
|
||||||
|
return this.urls.get(this.index.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fail(String url) {
|
||||||
|
int currentIndex = this.index.get();
|
||||||
|
if ((this.urls.get(currentIndex)).equals(url)) {
|
||||||
|
this.failures.put(currentIndex, Instant.now());
|
||||||
|
this.index.compareAndSet(currentIndex, (currentIndex + 1) % this.urls.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFailoverAvailable() {
|
||||||
|
var now = Instant.now();
|
||||||
|
return this.urls.size() > this.failures.size()
|
||||||
|
|| this.failures
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.anyMatch(e -> now.isAfter(e.plusMillis(retryGracePeriodInMs)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.urls.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.model;
|
package com.provectus.kafka.ui.model;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
@ -9,10 +8,21 @@ import lombok.Data;
|
||||||
public class InternalSchemaRegistry {
|
public class InternalSchemaRegistry {
|
||||||
private final String username;
|
private final String username;
|
||||||
private final String password;
|
private final String password;
|
||||||
private final List<String> url;
|
private final FailoverUrlList url;
|
||||||
|
|
||||||
public String getFirstUrl() {
|
public String getPrimaryNodeUri() {
|
||||||
return url != null && !url.isEmpty() ? url.iterator().next() : null;
|
return url.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUri() {
|
||||||
|
return url.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markAsUnavailable(String url) {
|
||||||
|
this.url.fail(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFailoverAvailable() {
|
||||||
|
return this.url.isFailoverAvailable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -71,7 +72,10 @@ public class SchemaRegistryAwareRecordSerDe implements RecordSerDe {
|
||||||
"You specified password but do not specified username");
|
"You specified password but do not specified username");
|
||||||
}
|
}
|
||||||
return new CachedSchemaRegistryClient(
|
return new CachedSchemaRegistryClient(
|
||||||
cluster.getSchemaRegistry().getUrl(),
|
cluster.getSchemaRegistry()
|
||||||
|
.getUrl()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toUnmodifiableList()),
|
||||||
1_000,
|
1_000,
|
||||||
schemaProviders,
|
schemaProviders,
|
||||||
configs
|
configs
|
||||||
|
@ -224,7 +228,7 @@ public class SchemaRegistryAwareRecordSerDe implements RecordSerDe {
|
||||||
private String convertSchema(SchemaMetadata schema) {
|
private String convertSchema(SchemaMetadata schema) {
|
||||||
|
|
||||||
String jsonSchema;
|
String jsonSchema;
|
||||||
URI basePath = new URI(cluster.getSchemaRegistry().getFirstUrl())
|
URI basePath = new URI(cluster.getSchemaRegistry().getPrimaryNodeUri())
|
||||||
.resolve(Integer.toString(schema.getId()));
|
.resolve(Integer.toString(schema.getId()));
|
||||||
final ParsedSchema schemaById = schemaRegistryClient.getSchemaById(schema.getId());
|
final ParsedSchema schemaById = schemaRegistryClient.getSchemaById(schema.getId());
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityCheck;
|
||||||
import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityLevel;
|
import com.provectus.kafka.ui.model.schemaregistry.InternalCompatibilityLevel;
|
||||||
import com.provectus.kafka.ui.model.schemaregistry.InternalNewSchema;
|
import com.provectus.kafka.ui.model.schemaregistry.InternalNewSchema;
|
||||||
import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse;
|
import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Formatter;
|
import java.util.Formatter;
|
||||||
|
@ -28,6 +29,7 @@ import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -42,6 +44,7 @@ import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.reactive.function.BodyInserters;
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -79,7 +82,9 @@ public class SchemaRegistryService {
|
||||||
URL_SUBJECTS)
|
URL_SUBJECTS)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(String[].class)
|
.bodyToMono(String[].class)
|
||||||
.doOnError(e -> log.error("Unexpected error", e));
|
.doOnError(e -> log.error("Unexpected error", e))
|
||||||
|
.as(m -> failoverAble(m,
|
||||||
|
new FailoverMono<>(cluster.getSchemaRegistry(), () -> this.getAllSubjectNames(cluster))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flux<SchemaSubjectDTO> getAllVersionsBySubject(KafkaCluster cluster, String subject) {
|
public Flux<SchemaSubjectDTO> getAllVersionsBySubject(KafkaCluster cluster, String subject) {
|
||||||
|
@ -96,7 +101,9 @@ public class SchemaRegistryService {
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(NOT_FOUND::equals,
|
.onStatus(NOT_FOUND::equals,
|
||||||
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
||||||
.bodyToFlux(Integer.class);
|
.bodyToFlux(Integer.class)
|
||||||
|
.as(f -> failoverAble(f, new FailoverFlux<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.getSubjectVersions(cluster, schemaName))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<SchemaSubjectDTO> getSchemaSubjectByVersion(KafkaCluster cluster, String schemaName,
|
public Mono<SchemaSubjectDTO> getSchemaSubjectByVersion(KafkaCluster cluster, String schemaName,
|
||||||
|
@ -114,7 +121,7 @@ public class SchemaRegistryService {
|
||||||
return configuredWebClient(
|
return configuredWebClient(
|
||||||
cluster,
|
cluster,
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
URL_SUBJECT_BY_VERSION,
|
SchemaRegistryService.URL_SUBJECT_BY_VERSION,
|
||||||
List.of(schemaName, version))
|
List.of(schemaName, version))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(NOT_FOUND::equals,
|
.onStatus(NOT_FOUND::equals,
|
||||||
|
@ -128,7 +135,9 @@ public class SchemaRegistryService {
|
||||||
String compatibilityLevel = tuple.getT2().getCompatibility().getValue();
|
String compatibilityLevel = tuple.getT2().getCompatibility().getValue();
|
||||||
schema.setCompatibilityLevel(compatibilityLevel);
|
schema.setCompatibilityLevel(compatibilityLevel);
|
||||||
return schema;
|
return schema;
|
||||||
});
|
})
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.getSchemaSubject(cluster, schemaName, version))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,14 +165,16 @@ public class SchemaRegistryService {
|
||||||
return configuredWebClient(
|
return configuredWebClient(
|
||||||
cluster,
|
cluster,
|
||||||
HttpMethod.DELETE,
|
HttpMethod.DELETE,
|
||||||
URL_SUBJECT_BY_VERSION,
|
SchemaRegistryService.URL_SUBJECT_BY_VERSION,
|
||||||
List.of(schemaName, version))
|
List.of(schemaName, version))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(NOT_FOUND::equals,
|
.onStatus(NOT_FOUND::equals,
|
||||||
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA_VERSION, schemaName, version))
|
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA_VERSION, schemaName, version))
|
||||||
)
|
)
|
||||||
.toBodilessEntity()
|
.toBodilessEntity()
|
||||||
.then();
|
.then()
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.deleteSchemaSubject(cluster, schemaName, version))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<Void> deleteSchemaSubjectEntirely(KafkaCluster cluster,
|
public Mono<Void> deleteSchemaSubjectEntirely(KafkaCluster cluster,
|
||||||
|
@ -176,7 +187,9 @@ public class SchemaRegistryService {
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(HttpStatus::isError, errorOnSchemaDeleteFailure(schemaName))
|
.onStatus(HttpStatus::isError, errorOnSchemaDeleteFailure(schemaName))
|
||||||
.toBodilessEntity()
|
.toBodilessEntity()
|
||||||
.then();
|
.then()
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.deleteSchemaSubjectEntirely(cluster, schemaName))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,8 +217,7 @@ public class SchemaRegistryService {
|
||||||
return configuredWebClient(
|
return configuredWebClient(
|
||||||
cluster,
|
cluster,
|
||||||
HttpMethod.POST,
|
HttpMethod.POST,
|
||||||
URL_SUBJECT_VERSIONS,
|
URL_SUBJECT_VERSIONS, subject)
|
||||||
subject)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(BodyInserters.fromPublisher(newSchemaSubject, InternalNewSchema.class))
|
.body(BodyInserters.fromPublisher(newSchemaSubject, InternalNewSchema.class))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
|
@ -214,7 +226,9 @@ public class SchemaRegistryService {
|
||||||
.flatMap(x -> Mono.error(isUnrecognizedFieldSchemaTypeMessage(x.getMessage())
|
.flatMap(x -> Mono.error(isUnrecognizedFieldSchemaTypeMessage(x.getMessage())
|
||||||
? new SchemaTypeNotSupportedException()
|
? new SchemaTypeNotSupportedException()
|
||||||
: new UnprocessableEntityException(x.getMessage()))))
|
: new UnprocessableEntityException(x.getMessage()))))
|
||||||
.bodyToMono(SubjectIdResponse.class);
|
.bodyToMono(SubjectIdResponse.class)
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> submitNewSchema(subject, newSchemaSubject, cluster))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -242,7 +256,9 @@ public class SchemaRegistryService {
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus(NOT_FOUND::equals,
|
.onStatus(NOT_FOUND::equals,
|
||||||
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
||||||
.bodyToMono(Void.class);
|
.bodyToMono(Void.class)
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.updateSchemaCompatibility(cluster, schemaName, compatibilityLevel))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<Void> updateSchemaCompatibility(KafkaCluster cluster,
|
public Mono<Void> updateSchemaCompatibility(KafkaCluster cluster,
|
||||||
|
@ -290,7 +306,9 @@ public class SchemaRegistryService {
|
||||||
.onStatus(NOT_FOUND::equals,
|
.onStatus(NOT_FOUND::equals,
|
||||||
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
|
||||||
.bodyToMono(InternalCompatibilityCheck.class)
|
.bodyToMono(InternalCompatibilityCheck.class)
|
||||||
.map(mapper::toCompatibilityCheckResponse);
|
.map(mapper::toCompatibilityCheckResponse)
|
||||||
|
.as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
|
||||||
|
() -> this.checksSchemaCompatibility(cluster, schemaName, newSchemaSubject))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String formatted(String str, Object... args) {
|
public String formatted(String str, Object... args) {
|
||||||
|
@ -318,7 +336,8 @@ public class SchemaRegistryService {
|
||||||
return errorMessage.contains(UNRECOGNIZED_FIELD_SCHEMA_TYPE);
|
return errorMessage.contains(UNRECOGNIZED_FIELD_SCHEMA_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, HttpMethod method, String uri) {
|
private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster, HttpMethod method,
|
||||||
|
String uri) {
|
||||||
return configuredWebClient(cluster, method, uri, Collections.emptyList(),
|
return configuredWebClient(cluster, method, uri, Collections.emptyList(),
|
||||||
new LinkedMultiValueMap<>());
|
new LinkedMultiValueMap<>());
|
||||||
}
|
}
|
||||||
|
@ -335,20 +354,20 @@ public class SchemaRegistryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster,
|
private WebClient.RequestBodySpec configuredWebClient(KafkaCluster cluster,
|
||||||
HttpMethod method, String uri,
|
HttpMethod method, String path,
|
||||||
List<String> uriVariables,
|
List<String> uriVariables,
|
||||||
MultiValueMap<String, String> queryParams) {
|
MultiValueMap<String, String> queryParams) {
|
||||||
final var schemaRegistry = cluster.getSchemaRegistry();
|
final var schemaRegistry = cluster.getSchemaRegistry();
|
||||||
return webClient
|
return webClient
|
||||||
.method(method)
|
.method(method)
|
||||||
.uri(buildUri(schemaRegistry, uri, uriVariables, queryParams))
|
.uri(buildUri(schemaRegistry, path, uriVariables, queryParams))
|
||||||
.headers(headers -> setBasicAuthIfEnabled(schemaRegistry, headers));
|
.headers(headers -> setBasicAuthIfEnabled(schemaRegistry, headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI buildUri(InternalSchemaRegistry schemaRegistry, String uri, List<String> uriVariables,
|
private URI buildUri(InternalSchemaRegistry schemaRegistry, String path, List<String> uriVariables,
|
||||||
MultiValueMap<String, String> queryParams) {
|
MultiValueMap<String, String> queryParams) {
|
||||||
final var builder = UriComponentsBuilder
|
final var builder = UriComponentsBuilder
|
||||||
.fromHttpUrl(schemaRegistry.getFirstUrl() + uri);
|
.fromHttpUrl(schemaRegistry.getUri() + path);
|
||||||
builder.queryParams(queryParams);
|
builder.queryParams(queryParams);
|
||||||
return builder.buildAndExpand(uriVariables.toArray()).toUri();
|
return builder.buildAndExpand(uriVariables.toArray()).toUri();
|
||||||
}
|
}
|
||||||
|
@ -361,4 +380,59 @@ public class SchemaRegistryService {
|
||||||
return Mono.error(new SchemaFailedToDeleteException(schemaName));
|
return Mono.error(new SchemaFailedToDeleteException(schemaName));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <T> Mono<T> failoverAble(Mono<T> request, FailoverMono<T> failoverMethod) {
|
||||||
|
return request.onErrorResume(failoverMethod::failover);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Flux<T> failoverAble(Flux<T> request, FailoverFlux<T> failoverMethod) {
|
||||||
|
return request.onErrorResume(failoverMethod::failover);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class Failover<E> {
|
||||||
|
private final InternalSchemaRegistry schemaRegistry;
|
||||||
|
private final Supplier<E> failover;
|
||||||
|
|
||||||
|
private Failover(InternalSchemaRegistry schemaRegistry, Supplier<E> failover) {
|
||||||
|
this.schemaRegistry = Objects.requireNonNull(schemaRegistry);
|
||||||
|
this.failover = Objects.requireNonNull(failover);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract E error(Throwable error);
|
||||||
|
|
||||||
|
public E failover(Throwable error) {
|
||||||
|
if (error instanceof WebClientRequestException
|
||||||
|
&& error.getCause() instanceof IOException
|
||||||
|
&& schemaRegistry.isFailoverAvailable()) {
|
||||||
|
var uri = ((WebClientRequestException) error).getUri();
|
||||||
|
schemaRegistry.markAsUnavailable(String.format("%s://%s", uri.getScheme(), uri.getAuthority()));
|
||||||
|
return failover.get();
|
||||||
|
}
|
||||||
|
return error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FailoverMono<T> extends Failover<Mono<T>> {
|
||||||
|
|
||||||
|
private FailoverMono(InternalSchemaRegistry schemaRegistry, Supplier<Mono<T>> failover) {
|
||||||
|
super(schemaRegistry, failover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Mono<T> error(Throwable error) {
|
||||||
|
return Mono.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FailoverFlux<T> extends Failover<Flux<T>> {
|
||||||
|
|
||||||
|
private FailoverFlux(InternalSchemaRegistry schemaRegistry, Supplier<Flux<T>> failover) {
|
||||||
|
super(schemaRegistry, failover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Flux<T> error(Throwable error) {
|
||||||
|
return Flux.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.util.SocketUtils;
|
||||||
import org.testcontainers.containers.KafkaContainer;
|
import org.testcontainers.containers.KafkaContainer;
|
||||||
import org.testcontainers.containers.Network;
|
import org.testcontainers.containers.Network;
|
||||||
import org.testcontainers.utility.DockerImageName;
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
@ -59,7 +60,9 @@ public abstract class AbstractIntegrationTest {
|
||||||
public void initialize(@NotNull ConfigurableApplicationContext context) {
|
public void initialize(@NotNull ConfigurableApplicationContext context) {
|
||||||
System.setProperty("kafka.clusters.0.name", LOCAL);
|
System.setProperty("kafka.clusters.0.name", LOCAL);
|
||||||
System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers());
|
System.setProperty("kafka.clusters.0.bootstrapServers", kafka.getBootstrapServers());
|
||||||
System.setProperty("kafka.clusters.0.schemaRegistry", schemaRegistry.getUrl());
|
// List unavailable hosts to verify failover
|
||||||
|
System.setProperty("kafka.clusters.0.schemaRegistry", String.format("http://localhost:%1$s,http://localhost:%1$s,%2$s",
|
||||||
|
SocketUtils.findAvailableTcpPort(), schemaRegistry.getUrl()));
|
||||||
System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect");
|
System.setProperty("kafka.clusters.0.kafkaConnect.0.name", "kafka-connect");
|
||||||
System.setProperty("kafka.clusters.0.kafkaConnect.0.address", kafkaConnect.getTarget());
|
System.setProperty("kafka.clusters.0.kafkaConnect.0.address", kafkaConnect.getTarget());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.provectus.kafka.ui.model;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
||||||
|
class FailoverUrlListTest {
|
||||||
|
|
||||||
|
public static final int RETRY_GRACE_PERIOD_IN_MS = 10;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class ShouldHaveFailoverAvailableWhen {
|
||||||
|
|
||||||
|
private FailoverUrlList failoverUrlList;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void before() {
|
||||||
|
failoverUrlList = new FailoverUrlList(List.of("localhost:123", "farawayhost:5678"), RETRY_GRACE_PERIOD_IN_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thereAreNoFailures() {
|
||||||
|
assertThat(failoverUrlList.isFailoverAvailable()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withLessFailuresThenAvailableUrls() {
|
||||||
|
failoverUrlList.fail(failoverUrlList.current());
|
||||||
|
|
||||||
|
assertThat(failoverUrlList.isFailoverAvailable()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withAllFailuresAndAtLeastOneAfterTheGraceTimeoutPeriod() throws InterruptedException {
|
||||||
|
failoverUrlList.fail(failoverUrlList.current());
|
||||||
|
failoverUrlList.fail(failoverUrlList.current());
|
||||||
|
|
||||||
|
Thread.sleep(RETRY_GRACE_PERIOD_IN_MS + 1);
|
||||||
|
|
||||||
|
assertThat(failoverUrlList.isFailoverAvailable()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class ShouldNotHaveFailoverAvailableWhen {
|
||||||
|
|
||||||
|
private FailoverUrlList failoverUrlList;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void before() {
|
||||||
|
failoverUrlList = new FailoverUrlList(List.of("localhost:123", "farawayhost:5678"), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allFailuresWithinGracePeriod() {
|
||||||
|
failoverUrlList.fail(failoverUrlList.current());
|
||||||
|
failoverUrlList.fail(failoverUrlList.current());
|
||||||
|
|
||||||
|
assertThat(failoverUrlList.isFailoverAvailable()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue