Reactive: add Sentinel Reactor adapter module (#545)
* Add reactive adapter implementation for Project Reactor (Mono/Flux) including a reactor transformer and an experimental `ReactorSphU` * Add an `InheritableBaseSubscriber` that derives from the original BaseSubscriber of reactor-core * Add basic test cases for reactor adapter * Add Sentinel context enter support for EntryConfig in reactor transformer Signed-off-by: Eric Zhao <sczyh16@gmail.com>
This commit is contained in:
parent
b8e295a138
commit
e857b5cef7
|
|
@ -19,6 +19,7 @@
|
||||||
<module>sentinel-dubbo-adapter</module>
|
<module>sentinel-dubbo-adapter</module>
|
||||||
<module>sentinel-grpc-adapter</module>
|
<module>sentinel-grpc-adapter</module>
|
||||||
<module>sentinel-zuul-adapter</module>
|
<module>sentinel-zuul-adapter</module>
|
||||||
|
<module>sentinel-reactor-adapter</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
<artifactId>sentinel-web-servlet</artifactId>
|
<artifactId>sentinel-web-servlet</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>sentinel-adapter</artifactId>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<version>1.5.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>sentinel-reactor-adapter</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.source.version>1.8</java.source.version>
|
||||||
|
<java.target.version>1.8</java.target.version>
|
||||||
|
<reactor.version>3.2.6.RELEASE</reactor.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-core</artifactId>
|
||||||
|
<version>${reactor.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-test</artifactId>
|
||||||
|
<version>${reactor.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||||
|
import com.alibaba.csp.sentinel.util.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
*/
|
||||||
|
public class ContextConfig {
|
||||||
|
|
||||||
|
private final String contextName;
|
||||||
|
private final String origin;
|
||||||
|
|
||||||
|
public ContextConfig(String contextName) {
|
||||||
|
this(contextName, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContextConfig(String contextName, String origin) {
|
||||||
|
AssertUtil.assertNotBlank(contextName, "contextName cannot be blank");
|
||||||
|
this.contextName = contextName;
|
||||||
|
if (StringUtil.isBlank(origin)) {
|
||||||
|
origin = "";
|
||||||
|
}
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContextName() {
|
||||||
|
return contextName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrigin() {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ContextConfig{" +
|
||||||
|
"contextName='" + contextName + '\'' +
|
||||||
|
", origin='" + origin + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.EntryType;
|
||||||
|
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public class EntryConfig {
|
||||||
|
|
||||||
|
private final String resourceName;
|
||||||
|
private final EntryType entryType;
|
||||||
|
private final ContextConfig contextConfig;
|
||||||
|
|
||||||
|
public EntryConfig(String resourceName) {
|
||||||
|
this(resourceName, EntryType.OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryConfig(String resourceName, EntryType entryType) {
|
||||||
|
this(resourceName, entryType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryConfig(String resourceName, EntryType entryType, ContextConfig contextConfig) {
|
||||||
|
checkParams(resourceName, entryType);
|
||||||
|
this.resourceName = resourceName;
|
||||||
|
this.entryType = entryType;
|
||||||
|
// Constructed ContextConfig should be valid here. Null is allowed here.
|
||||||
|
this.contextConfig = contextConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceName() {
|
||||||
|
return resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryType getEntryType() {
|
||||||
|
return entryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContextConfig getContextConfig() {
|
||||||
|
return contextConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertValid(EntryConfig config) {
|
||||||
|
AssertUtil.notNull(config, "entry config cannot be null");
|
||||||
|
checkParams(config.resourceName, config.entryType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkParams(String resourceName, EntryType entryType) {
|
||||||
|
AssertUtil.assertNotBlank(resourceName, "resourceName cannot be blank");
|
||||||
|
AssertUtil.notNull(entryType, "entryType cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "EntryConfig{" +
|
||||||
|
"resourceName='" + resourceName + '\'' +
|
||||||
|
", entryType=" + entryType +
|
||||||
|
", contextConfig=" + contextConfig +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import reactor.core.CoreSubscriber;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.FluxOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public class FluxSentinelOperator<T> extends FluxOperator<T, T> {
|
||||||
|
|
||||||
|
private final EntryConfig entryConfig;
|
||||||
|
|
||||||
|
public FluxSentinelOperator(Flux<? extends T> source, EntryConfig entryConfig) {
|
||||||
|
super(source);
|
||||||
|
EntryConfig.assertValid(entryConfig);
|
||||||
|
this.entryConfig = entryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void subscribe(CoreSubscriber<? super T> actual) {
|
||||||
|
source.subscribe(new SentinelReactorSubscriber<>(entryConfig, actual, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import reactor.core.CoreSubscriber;
|
||||||
|
import reactor.core.Disposable;
|
||||||
|
import reactor.core.Exceptions;
|
||||||
|
import reactor.core.publisher.Operators;
|
||||||
|
import reactor.core.publisher.SignalType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Copied from {@link reactor.core.publisher.BaseSubscriber} of reactor-core,
|
||||||
|
* but allow sub-classes to override {@code onSubscribe}, {@code onNext},
|
||||||
|
* {@code onError} and {@code onComplete} method for customization.
|
||||||
|
* </p>
|
||||||
|
* <p>This base subscriber also provides predicate for {@code onErrorDropped} hook as a workaround for Sentinel.</p>
|
||||||
|
*/
|
||||||
|
abstract class InheritableBaseSubscriber<T> implements CoreSubscriber<T>, Subscription, Disposable {
|
||||||
|
|
||||||
|
volatile Subscription subscription;
|
||||||
|
|
||||||
|
static AtomicReferenceFieldUpdater<InheritableBaseSubscriber, Subscription> S =
|
||||||
|
AtomicReferenceFieldUpdater.newUpdater(InheritableBaseSubscriber.class, Subscription.class,
|
||||||
|
"subscription");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current {@link Subscription}
|
||||||
|
*
|
||||||
|
* @return current {@link Subscription}
|
||||||
|
*/
|
||||||
|
protected Subscription upstream() {
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDisposed() {
|
||||||
|
return subscription == Operators.cancelledSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Disposable#dispose() Dispose} the {@link Subscription} by
|
||||||
|
* {@link Subscription#cancel() cancelling} it.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for further processing of onSubscribe's Subscription. Implement this method
|
||||||
|
* to call {@link #request(long)} as an initial request. Values other than the
|
||||||
|
* unbounded {@code Long.MAX_VALUE} imply that you'll also call request in
|
||||||
|
* {@link #hookOnNext(Object)}.
|
||||||
|
* <p> Defaults to request unbounded Long.MAX_VALUE as in {@link #requestUnbounded()}
|
||||||
|
*
|
||||||
|
* @param subscription the subscription to optionally process
|
||||||
|
*/
|
||||||
|
protected void hookOnSubscribe(Subscription subscription) {
|
||||||
|
subscription.request(Long.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for processing of onNext values. You can call {@link #request(long)} here
|
||||||
|
* to further request data from the source {@code org.reactivestreams.Publisher} if
|
||||||
|
* the {@link #hookOnSubscribe(Subscription) initial request} wasn't unbounded.
|
||||||
|
* <p>Defaults to doing nothing.
|
||||||
|
*
|
||||||
|
* @param value the emitted value to process
|
||||||
|
*/
|
||||||
|
protected void hookOnNext(T value) {
|
||||||
|
// NO-OP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional hook for completion processing. Defaults to doing nothing.
|
||||||
|
*/
|
||||||
|
protected void hookOnComplete() {
|
||||||
|
// NO-OP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional hook for error processing. Default is to call
|
||||||
|
* {@link Exceptions#errorCallbackNotImplemented(Throwable)}.
|
||||||
|
*
|
||||||
|
* @param throwable the error to process
|
||||||
|
*/
|
||||||
|
protected void hookOnError(Throwable throwable) {
|
||||||
|
throw Exceptions.errorCallbackNotImplemented(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional hook executed when the subscription is cancelled by calling this
|
||||||
|
* Subscriber's {@link #cancel()} method. Defaults to doing nothing.
|
||||||
|
*/
|
||||||
|
protected void hookOnCancel() {
|
||||||
|
//NO-OP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional hook executed after any of the termination events (onError, onComplete,
|
||||||
|
* cancel). The hook is executed in addition to and after {@link #hookOnError(Throwable)},
|
||||||
|
* {@link #hookOnComplete()} and {@link #hookOnCancel()} hooks, even if these callbacks
|
||||||
|
* fail. Defaults to doing nothing. A failure of the callback will be caught by
|
||||||
|
* {@code Operators#onErrorDropped(Throwable, reactor.util.context.Context)}.
|
||||||
|
*
|
||||||
|
* @param type the type of termination event that triggered the hook
|
||||||
|
* ({@link SignalType#ON_ERROR}, {@link SignalType#ON_COMPLETE} or
|
||||||
|
* {@link SignalType#CANCEL})
|
||||||
|
*/
|
||||||
|
protected void hookFinally(SignalType type) {
|
||||||
|
//NO-OP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription s) {
|
||||||
|
if (Operators.setOnce(S, this, s)) {
|
||||||
|
try {
|
||||||
|
hookOnSubscribe(s);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
onError(Operators.onOperatorError(s, throwable, currentContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(T value) {
|
||||||
|
Objects.requireNonNull(value, "onNext");
|
||||||
|
try {
|
||||||
|
hookOnNext(value);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
onError(Operators.onOperatorError(subscription, throwable, value, currentContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean shouldCallErrorDropHook() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable t) {
|
||||||
|
Objects.requireNonNull(t, "onError");
|
||||||
|
|
||||||
|
if (S.getAndSet(this, Operators.cancelledSubscription()) == Operators
|
||||||
|
.cancelledSubscription()) {
|
||||||
|
// Already cancelled concurrently
|
||||||
|
|
||||||
|
// Workaround for Sentinel BlockException:
|
||||||
|
// Here we add a predicate method to decide whether exception should be dropped implicitly
|
||||||
|
// or call the {@code onErrorDropped} hook.
|
||||||
|
if (shouldCallErrorDropHook()) {
|
||||||
|
Operators.onErrorDropped(t, currentContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
hookOnError(t);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e = Exceptions.addSuppressed(e, t);
|
||||||
|
Operators.onErrorDropped(e, currentContext());
|
||||||
|
} finally {
|
||||||
|
safeHookFinally(SignalType.ON_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
if (S.getAndSet(this, Operators.cancelledSubscription()) != Operators
|
||||||
|
.cancelledSubscription()) {
|
||||||
|
//we're sure it has not been concurrently cancelled
|
||||||
|
try {
|
||||||
|
hookOnComplete();
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
//onError itself will short-circuit due to the CancelledSubscription being push above
|
||||||
|
hookOnError(Operators.onOperatorError(throwable, currentContext()));
|
||||||
|
} finally {
|
||||||
|
safeHookFinally(SignalType.ON_COMPLETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void request(long n) {
|
||||||
|
if (Operators.validate(n)) {
|
||||||
|
Subscription s = this.subscription;
|
||||||
|
if (s != null) {
|
||||||
|
s.request(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #request(long) Request} an unbounded amount.
|
||||||
|
*/
|
||||||
|
public final void requestUnbounded() {
|
||||||
|
request(Long.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void cancel() {
|
||||||
|
if (Operators.terminate(S, this)) {
|
||||||
|
try {
|
||||||
|
hookOnCancel();
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
hookOnError(Operators.onOperatorError(subscription, throwable, currentContext()));
|
||||||
|
} finally {
|
||||||
|
safeHookFinally(SignalType.CANCEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void safeHookFinally(SignalType type) {
|
||||||
|
try {
|
||||||
|
hookFinally(type);
|
||||||
|
} catch (Throwable finallyFailure) {
|
||||||
|
Operators.onErrorDropped(finallyFailure, currentContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import reactor.core.CoreSubscriber;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.MonoOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public class MonoSentinelOperator<T> extends MonoOperator<T, T> {
|
||||||
|
|
||||||
|
private final EntryConfig entryConfig;
|
||||||
|
|
||||||
|
public MonoSentinelOperator(Mono<? extends T> source, EntryConfig entryConfig) {
|
||||||
|
super(source);
|
||||||
|
EntryConfig.assertValid(entryConfig);
|
||||||
|
this.entryConfig = entryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void subscribe(CoreSubscriber<? super T> actual) {
|
||||||
|
source.subscribe(new SentinelReactorSubscriber<>(entryConfig, actual, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.AsyncEntry;
|
||||||
|
import com.alibaba.csp.sentinel.EntryType;
|
||||||
|
import com.alibaba.csp.sentinel.SphU;
|
||||||
|
import com.alibaba.csp.sentinel.Tracer;
|
||||||
|
import com.alibaba.csp.sentinel.context.Context;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SphU} adapter with Project Reactor.
|
||||||
|
*
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public final class ReactorSphU {
|
||||||
|
|
||||||
|
public static <R> Mono<R> entryWith(String resourceName, Mono<R> actual) {
|
||||||
|
return entryWith(resourceName, EntryType.OUT, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <R> Mono<R> entryWith(String resourceName, EntryType entryType, Mono<R> actual) {
|
||||||
|
final AtomicReference<AsyncEntry> entryWrapper = new AtomicReference<>(null);
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
try {
|
||||||
|
AsyncEntry entry = SphU.asyncEntry(resourceName, entryType);
|
||||||
|
entryWrapper.set(entry);
|
||||||
|
return actual.subscriberContext(context -> {
|
||||||
|
if (entry == null) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
Context sentinelContext = entry.getAsyncContext();
|
||||||
|
if (sentinelContext == null) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
// TODO: check GC friendly?
|
||||||
|
return context.put(SentinelReactorConstants.SENTINEL_CONTEXT_KEY, sentinelContext);
|
||||||
|
}).doOnSuccessOrError((o, t) -> {
|
||||||
|
if (entry != null && entryWrapper.compareAndSet(entry, null)) {
|
||||||
|
if (t != null) {
|
||||||
|
Tracer.traceContext(t, 1, entry.getAsyncContext());
|
||||||
|
}
|
||||||
|
entry.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (BlockException ex) {
|
||||||
|
return Mono.error(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactorSphU() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public final class SentinelReactorConstants {
|
||||||
|
|
||||||
|
public static final String SENTINEL_CONTEXT_KEY = "_sentinel_context";
|
||||||
|
|
||||||
|
private SentinelReactorConstants() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.AsyncEntry;
|
||||||
|
import com.alibaba.csp.sentinel.SphU;
|
||||||
|
import com.alibaba.csp.sentinel.Tracer;
|
||||||
|
import com.alibaba.csp.sentinel.context.ContextUtil;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.alibaba.csp.sentinel.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import reactor.core.CoreSubscriber;
|
||||||
|
import reactor.util.context.Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public class SentinelReactorSubscriber<T> extends InheritableBaseSubscriber<T> {
|
||||||
|
|
||||||
|
private final EntryConfig entryConfig;
|
||||||
|
|
||||||
|
private final CoreSubscriber<? super T> actual;
|
||||||
|
private final boolean unary;
|
||||||
|
|
||||||
|
private volatile AsyncEntry currentEntry;
|
||||||
|
private final AtomicBoolean entryExited = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
public SentinelReactorSubscriber(EntryConfig entryConfig,
|
||||||
|
CoreSubscriber<? super T> actual,
|
||||||
|
boolean unary) {
|
||||||
|
checkEntryConfig(entryConfig);
|
||||||
|
this.entryConfig = entryConfig;
|
||||||
|
this.actual = actual;
|
||||||
|
this.unary = unary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkEntryConfig(EntryConfig config) {
|
||||||
|
EntryConfig.assertValid(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Context currentContext() {
|
||||||
|
if (currentEntry == null || entryExited.get()) {
|
||||||
|
return actual.currentContext();
|
||||||
|
}
|
||||||
|
com.alibaba.csp.sentinel.context.Context sentinelContext = currentEntry.getAsyncContext();
|
||||||
|
if (sentinelContext == null) {
|
||||||
|
return actual.currentContext();
|
||||||
|
}
|
||||||
|
return actual.currentContext()
|
||||||
|
.put(SentinelReactorConstants.SENTINEL_CONTEXT_KEY, currentEntry.getAsyncContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doWithContextOrCurrent(Supplier<Optional<com.alibaba.csp.sentinel.context.Context>> contextSupplier,
|
||||||
|
Runnable f) {
|
||||||
|
Optional<com.alibaba.csp.sentinel.context.Context> contextOpt = contextSupplier.get();
|
||||||
|
if (!contextOpt.isPresent()) {
|
||||||
|
// Provided context is absent, use current context.
|
||||||
|
f.run();
|
||||||
|
} else {
|
||||||
|
// Run on provided context.
|
||||||
|
ContextUtil.runOnContext(contextOpt.get(), f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void entryWhenSubscribed() {
|
||||||
|
ContextConfig sentinelContextConfig = entryConfig.getContextConfig();
|
||||||
|
if (sentinelContextConfig != null) {
|
||||||
|
// If current we're already in a context, the context config won't work.
|
||||||
|
ContextUtil.enter(sentinelContextConfig.getContextName(), sentinelContextConfig.getOrigin());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AsyncEntry entry = SphU.asyncEntry(entryConfig.getResourceName());
|
||||||
|
this.currentEntry = entry;
|
||||||
|
actual.onSubscribe(this);
|
||||||
|
} catch (BlockException ex) {
|
||||||
|
// Mark as completed (exited) explicitly.
|
||||||
|
entryExited.set(true);
|
||||||
|
// Signal cancel and propagate the {@code BlockException}.
|
||||||
|
cancel();
|
||||||
|
actual.onSubscribe(this);
|
||||||
|
actual.onError(ex);
|
||||||
|
} finally {
|
||||||
|
if (sentinelContextConfig != null) {
|
||||||
|
ContextUtil.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookOnSubscribe(Subscription subscription) {
|
||||||
|
doWithContextOrCurrent(() -> currentContext().getOrEmpty(SentinelReactorConstants.SENTINEL_CONTEXT_KEY),
|
||||||
|
this::entryWhenSubscribed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookOnNext(T value) {
|
||||||
|
if (isDisposed()) {
|
||||||
|
tryCompleteEntry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doWithContextOrCurrent(() -> Optional.ofNullable(currentEntry).map(AsyncEntry::getAsyncContext),
|
||||||
|
() -> actual.onNext(value));
|
||||||
|
|
||||||
|
if (unary) {
|
||||||
|
// For some cases of unary operator (Mono), we have to do this during onNext hook.
|
||||||
|
// e.g. this kind of order: onSubscribe() -> onNext() -> cancel() -> onComplete()
|
||||||
|
// the onComplete hook will not be executed so we'll need to complete the entry in advance.
|
||||||
|
tryCompleteEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookOnComplete() {
|
||||||
|
tryCompleteEntry();
|
||||||
|
actual.onComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldCallErrorDropHook() {
|
||||||
|
// When flow control triggered or stream terminated, the incoming
|
||||||
|
// deprecated exceptions should be dropped implicitly, so we'll not call the `onErrorDropped` hook.
|
||||||
|
return !entryExited.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookOnError(Throwable t) {
|
||||||
|
if (currentEntry != null && currentEntry.getAsyncContext() != null) {
|
||||||
|
// Normal requests with non-BlockException will go through here.
|
||||||
|
Tracer.traceContext(t, 1, currentEntry.getAsyncContext());
|
||||||
|
}
|
||||||
|
tryCompleteEntry();
|
||||||
|
actual.onError(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookOnCancel() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryCompleteEntry() {
|
||||||
|
if (currentEntry != null && entryExited.compareAndSet(false, true)) {
|
||||||
|
currentEntry.exit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 1999-2019 Alibaba Group Holding Ltd.
|
||||||
|
*
|
||||||
|
* Licensed 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.
|
||||||
|
*/
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transformer that transforms given {@code Publisher} to a wrapped Sentinel reactor operator.
|
||||||
|
*
|
||||||
|
* @author Eric Zhao
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
public class SentinelReactorTransformer<T> implements Function<Publisher<T>, Publisher<T>> {
|
||||||
|
|
||||||
|
private final EntryConfig entryConfig;
|
||||||
|
|
||||||
|
public SentinelReactorTransformer(String resourceName) {
|
||||||
|
this(new EntryConfig(resourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SentinelReactorTransformer(EntryConfig entryConfig) {
|
||||||
|
EntryConfig.assertValid(entryConfig);
|
||||||
|
this.entryConfig = entryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Publisher<T> apply(Publisher<T> publisher) {
|
||||||
|
if (publisher instanceof Mono) {
|
||||||
|
return new MonoSentinelOperator<>((Mono<T>) publisher, entryConfig);
|
||||||
|
}
|
||||||
|
if (publisher instanceof Flux) {
|
||||||
|
return new FluxSentinelOperator<>((Flux<T>) publisher, entryConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Publisher type is not supported: " + publisher.getClass().getCanonicalName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||||
|
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
*/
|
||||||
|
public class FluxSentinelOperatorTestIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitMultipleValueSuccess() {
|
||||||
|
String resourceName = createResourceName("testEmitMultipleSuccess");
|
||||||
|
StepVerifier.create(Flux.just(1, 2)
|
||||||
|
.map(e -> e * 2)
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectNext(2)
|
||||||
|
.expectNext(4)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitFluxError() {
|
||||||
|
String resourceName = createResourceName("testEmitFluxError");
|
||||||
|
StepVerifier.create(Flux.error(new IllegalAccessException("oops"))
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectError(IllegalAccessException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps());
|
||||||
|
assertEquals(1, cn.totalException());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitMultipleValuesWhenFlowControlTriggered() {
|
||||||
|
String resourceName = createResourceName("testEmitMultipleValuesWhenFlowControlTriggered");
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(
|
||||||
|
new FlowRule(resourceName).setCount(0)
|
||||||
|
));
|
||||||
|
StepVerifier.create(Flux.just(1, 3, 5)
|
||||||
|
.map(e -> e * 2)
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectError(BlockException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(0, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest());
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createResourceName(String resourceName) {
|
||||||
|
return "reactor_test_flux_" + resourceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.Constants;
|
||||||
|
import com.alibaba.csp.sentinel.EntryType;
|
||||||
|
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||||
|
import com.alibaba.csp.sentinel.node.EntranceNode;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||||
|
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
*/
|
||||||
|
public class MonoSentinelOperatorIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformMonoWithSentinelContextEnter() {
|
||||||
|
String resourceName = createResourceName("testTransformMonoWithSentinelContextEnter");
|
||||||
|
String contextName = "test_reactive_context";
|
||||||
|
String origin = "originA";
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(
|
||||||
|
new FlowRule(resourceName).setCount(0).setLimitApp(origin).as(FlowRule.class)
|
||||||
|
));
|
||||||
|
StepVerifier.create(Mono.just(2)
|
||||||
|
.transform(new SentinelReactorTransformer<>(
|
||||||
|
// Customized context with origin.
|
||||||
|
new EntryConfig(resourceName, EntryType.OUT, new ContextConfig(contextName, origin))))
|
||||||
|
)
|
||||||
|
.expectError(BlockException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(0, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest());
|
||||||
|
assertTrue(Constants.ROOT.getChildList()
|
||||||
|
.stream()
|
||||||
|
.filter(node -> node instanceof EntranceNode)
|
||||||
|
.map(e -> (EntranceNode)e)
|
||||||
|
.anyMatch(e -> e.getId().getName().equals(contextName))
|
||||||
|
);
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFluxToMonoNextThenCancelSuccess() {
|
||||||
|
String resourceName = createResourceName("testFluxToMonoNextThenCancelSuccess");
|
||||||
|
StepVerifier.create(Flux.range(1, 10)
|
||||||
|
.map(e -> e * 2)
|
||||||
|
.next()
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectNext(2)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitSingleLongTimeRt() {
|
||||||
|
String resourceName = createResourceName("testEmitSingleLongTimeRt");
|
||||||
|
StepVerifier.create(Mono.just(2)
|
||||||
|
.delayElement(Duration.ofMillis(1000))
|
||||||
|
.map(e -> e * 2)
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectNext(4)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1000, cn.avgRt(), 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitEmptySuccess() {
|
||||||
|
String resourceName = createResourceName("testEmitEmptySuccess");
|
||||||
|
StepVerifier.create(Mono.empty()
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitSingleSuccess() {
|
||||||
|
String resourceName = createResourceName("testEmitSingleSuccess");
|
||||||
|
StepVerifier.create(Mono.just(1)
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectNext(1)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitSingleValueWhenFlowControlTriggered() {
|
||||||
|
String resourceName = createResourceName("testEmitSingleValueWhenFlowControlTriggered");
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(
|
||||||
|
new FlowRule(resourceName).setCount(0)
|
||||||
|
));
|
||||||
|
StepVerifier.create(Mono.just(1)
|
||||||
|
.map(e -> e * 2)
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectError(BlockException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(0, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest());
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitExceptionWhenFlowControlTriggered() {
|
||||||
|
String resourceName = createResourceName("testEmitExceptionWhenFlowControlTriggered");
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(
|
||||||
|
new FlowRule(resourceName).setCount(0)
|
||||||
|
));
|
||||||
|
StepVerifier.create(Mono.error(new IllegalStateException("some"))
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectError(BlockException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(0, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest());
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmitSingleError() {
|
||||||
|
String resourceName = createResourceName("testEmitSingleError");
|
||||||
|
StepVerifier.create(Mono.error(new IllegalStateException())
|
||||||
|
.transform(new SentinelReactorTransformer<>(resourceName)))
|
||||||
|
.expectError(IllegalStateException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.totalException());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createResourceName(String resourceName) {
|
||||||
|
return "reactor_test_mono_" + resourceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.alibaba.csp.sentinel.adapter.reactor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.node.ClusterNode;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
|
||||||
|
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Eric Zhao
|
||||||
|
*/
|
||||||
|
public class ReactorSphUTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReactorEntryNormalWhenFlowControlTriggered() {
|
||||||
|
String resourceName = createResourceName("testReactorEntryNormalWhenFlowControlTriggered");
|
||||||
|
FlowRuleManager.loadRules(Collections.singletonList(
|
||||||
|
new FlowRule(resourceName).setCount(0)
|
||||||
|
));
|
||||||
|
StepVerifier.create(ReactorSphU.entryWith(resourceName, Mono.just(60))
|
||||||
|
.subscribeOn(Schedulers.elastic())
|
||||||
|
.map(e -> e * 3))
|
||||||
|
.expectError(BlockException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(0, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.blockRequest());
|
||||||
|
|
||||||
|
FlowRuleManager.loadRules(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReactorEntryWithCommon() {
|
||||||
|
String resourceName = createResourceName("testReactorEntryWithCommon");
|
||||||
|
StepVerifier.create(ReactorSphU.entryWith(resourceName, Mono.just(60))
|
||||||
|
.subscribeOn(Schedulers.elastic())
|
||||||
|
.map(e -> e * 3))
|
||||||
|
.expectNext(180)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReactorEntryWithBizException() {
|
||||||
|
String resourceName = createResourceName("testReactorEntryWithBizException");
|
||||||
|
StepVerifier.create(ReactorSphU.entryWith(resourceName, Mono.error(new IllegalStateException())))
|
||||||
|
.expectError(IllegalStateException.class)
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName);
|
||||||
|
assertNotNull(cn);
|
||||||
|
assertEquals(1, cn.passQps(), 0.01);
|
||||||
|
assertEquals(1, cn.totalException());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createResourceName(String resourceName) {
|
||||||
|
return "reactor_test_SphU_" + resourceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue