* 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>master
@@ -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; | |||||
} | |||||
} |