Fix some connection errors

This commit is contained in:
Andrea Cavalli 2022-10-12 18:31:44 +02:00
parent 58770ca649
commit 677ceb70a1
9 changed files with 131 additions and 97 deletions

View File

@ -148,7 +148,7 @@ public class AtomixReactiveApi implements ReactiveApi {
if (publisher != null) { if (publisher != null) {
publisher.handleRequest(req.data()); publisher.handleRequest(req.data());
} else { } else {
LOG.error("Dropped request because no session is found: {}", req); LOG.debug("Dropped request because no session is found: {}", req);
} }
}) })
.subscribeOn(Schedulers.parallel()) .subscribeOn(Schedulers.parallel())

View File

@ -67,6 +67,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.publisher.FluxSink.OverflowStrategy;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks.EmitFailureHandler; import reactor.core.publisher.Sinks.EmitFailureHandler;
import reactor.core.publisher.Sinks.EmitResult;
import reactor.core.publisher.Sinks.Many; import reactor.core.publisher.Sinks.Many;
import reactor.core.scheduler.Scheduler.Worker; import reactor.core.scheduler.Scheduler.Worker;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -503,8 +504,21 @@ public abstract class ReactiveApiPublisher {
public void handleRequest(OnRequest<TdApi.Object> onRequestObj) { public void handleRequest(OnRequest<TdApi.Object> onRequestObj) {
handleRequestInternal(onRequestObj, response -> { handleRequestInternal(onRequestObj, response -> {
EmitResult status;
synchronized (this.responses) { synchronized (this.responses) {
this.responses.emitNext(response, EmitFailureHandler.FAIL_FAST); status = this.responses.tryEmitNext(response);
}
if (status.isFailure()) {
switch (status) {
case FAIL_ZERO_SUBSCRIBER ->
LOG.warn("Failed to send response of request {}, user {}, client {}: no subscribers",
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId());
case FAIL_OVERFLOW ->
LOG.warn("Failed to send response of request {}, user {}, client {}: too many unsent responses",
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId());
default -> LOG.error("Failed to send response of request {}, user {}, client {}: {}",
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId(), status);
}
} }
}); });
} }

View File

@ -185,16 +185,28 @@ public class ReactorUtils {
} }
} }
); );
AtomicReference<Throwable> startEx = new AtomicReference<>();
var disposable = flux var disposable = flux
.subscribeOn(Schedulers.parallel()) .subscribeOn(Schedulers.parallel())
.publishOn(Schedulers.boundedElastic()) .publishOn(Schedulers.boundedElastic())
.subscribe(queue::add); .subscribe(queue::add, ex -> {
startEx.set(ex);
var refVal = ref.get();
if (refVal != null) {
refVal.error(ex);
}
});
queue.startQueue(); queue.startQueue();
return Flux.create(sink -> { return Flux.create(sink -> {
sink.onDispose(() -> { sink.onDispose(() -> {
disposable.dispose(); disposable.dispose();
queue.close(); queue.close();
}); });
var startExVal = startEx.get();
if (startExVal != null) {
sink.error(startExVal);
return;
}
ref.set(sink); ref.set(sink);
sink.onCancel(() -> ref.set(null)); sink.onCancel(() -> ref.set(null));
}); });

View File

@ -99,7 +99,9 @@ public class Stats extends Thread {
processedUpdatesRateSum += processedUpdatesRate; processedUpdatesRateSum += processedUpdatesRate;
clientBoundEventsRateSum += clientBoundEventsRate; clientBoundEventsRateSum += clientBoundEventsRate;
sentClientBoundEventsRateSum += sentClientBoundEventsRate; sentClientBoundEventsRateSum += sentClientBoundEventsRate;
out.append(String.format("%d:\t", clientIds.getLong(i))); if (LOG.isTraceEnabled()) {
out.append(String.format("%d:\t", clientIds.getLong(i)));
}
} else { } else {
receivedUpdatesRate = receivedUpdatesRateSum; receivedUpdatesRate = receivedUpdatesRateSum;
diskBuffered = diskBufferedSum; diskBuffered = diskBufferedSum;
@ -110,16 +112,18 @@ public class Stats extends Thread {
sentClientBoundEventsRate = sentClientBoundEventsRateSum; sentClientBoundEventsRate = sentClientBoundEventsRateSum;
out.append("Total:\t"); out.append("Total:\t");
} }
out.append(String.format( if (i == currentClients || LOG.isTraceEnabled()) {
"\tUpdates:\t[received %03.2fHz\tbuffered: %03.2fHz (RAM: %d HDD: %d)\tprocessed: %03.2fHz]\tClient bound events: %03.2fHz\tProcessed events: %03.2fHz\t%n", out.append(String.format(
receivedUpdatesRate, "\tUpdates:\t[received %03.2fHz\tbuffered: %03.2fHz (RAM: %d HDD: %d)\tprocessed: %03.2fHz]\tClient bound events: %03.2fHz\tProcessed events: %03.2fHz\t%n",
bufferedUpdatesRate, receivedUpdatesRate,
ramBuffered, bufferedUpdatesRate,
diskBuffered, ramBuffered,
processedUpdatesRate, diskBuffered,
clientBoundEventsRate, processedUpdatesRate,
sentClientBoundEventsRate clientBoundEventsRate,
)); sentClientBoundEventsRate
));
}
} }
out.append(String.format("%n")); out.append(String.format("%n"));

View File

@ -47,14 +47,13 @@ public class TdlibChannelsSharedHost implements Closeable {
private final TdlibChannelsServers tdServersChannels; private final TdlibChannelsServers tdServersChannels;
private final Disposable responsesSub; private final Disposable responsesSub;
private final AtomicReference<Disposable> requestsSub = new AtomicReference<>(); private final AtomicReference<Disposable> requestsSub = new AtomicReference<>();
private final Many<OnResponse<TdApi.Object>> responses = Sinks.many().multicast().directAllOrNothing(); private final Many<OnResponse<TdApi.Object>> responses = Sinks.many().multicast().onBackpressureBuffer(65_535);
private final Map<String, Many<Flux<ClientBoundEvent>>> events; private final Map<String, Many<Flux<ClientBoundEvent>>> events;
private final Flux<Timestamped<OnRequest<Object>>> requests; private final Flux<Timestamped<OnRequest<Object>>> requests;
public TdlibChannelsSharedHost(Set<String> allLanes, TdlibChannelsServers tdServersChannels) { public TdlibChannelsSharedHost(Set<String> allLanes, TdlibChannelsServers tdServersChannels) {
this.tdServersChannels = tdServersChannels; this.tdServersChannels = tdServersChannels;
this.responsesSub = Mono.defer(() -> tdServersChannels.response() this.responsesSub = tdServersChannels.response().sendMessages(responses.asFlux()/*.log("responses", Level.FINE)*/)
.sendMessages(responses.asFlux()/*.log("responses", Level.FINE)*/))
.repeatWhen(REPEAT_STRATEGY) .repeatWhen(REPEAT_STRATEGY)
.retryWhen(RETRY_STRATEGY) .retryWhen(RETRY_STRATEGY)
.subscribeOn(Schedulers.parallel()) .subscribeOn(Schedulers.parallel())
@ -64,8 +63,7 @@ public class TdlibChannelsSharedHost implements Closeable {
Flux<ClientBoundEvent> outputEventsFlux = Flux Flux<ClientBoundEvent> outputEventsFlux = Flux
.merge(sink.asFlux().map(flux -> flux.publishOn(Schedulers.parallel())), Integer.MAX_VALUE, 256) .merge(sink.asFlux().map(flux -> flux.publishOn(Schedulers.parallel())), Integer.MAX_VALUE, 256)
.doFinally(s -> LOG.debug("Output events flux of lane \"{}\" terminated with signal {}", lane, s)); .doFinally(s -> LOG.debug("Output events flux of lane \"{}\" terminated with signal {}", lane, s));
Mono.defer(() -> tdServersChannels.events(lane) tdServersChannels.events(lane).sendMessages(outputEventsFlux)
.sendMessages(outputEventsFlux))
.repeatWhen(REPEAT_STRATEGY) .repeatWhen(REPEAT_STRATEGY)
.retryWhen(RETRY_STRATEGY) .retryWhen(RETRY_STRATEGY)
.subscribeOn(Schedulers.parallel()) .subscribeOn(Schedulers.parallel())
@ -92,9 +90,9 @@ public class TdlibChannelsSharedHost implements Closeable {
if (eventsSink == null) { if (eventsSink == null) {
throw new IllegalArgumentException("Lane " + lane + " does not exist"); throw new IllegalArgumentException("Lane " + lane + " does not exist");
} }
eventsSink.emitNext(eventFlux.takeUntilOther(canceller.asMono()), synchronized (events) {
EmitFailureHandler.busyLooping(Duration.ofMillis(100)) eventsSink.emitNext(eventFlux.takeUntilOther(canceller.asMono()), EmitFailureHandler.FAIL_FAST);
); }
return () -> canceller.tryEmitEmpty(); return () -> canceller.tryEmitEmpty();
} }

View File

@ -15,7 +15,6 @@ import reactor.core.publisher.Sinks.EmitFailureHandler;
import reactor.core.publisher.Sinks.Empty; import reactor.core.publisher.Sinks.Empty;
import reactor.core.publisher.Sinks.Many; import reactor.core.publisher.Sinks.Many;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
import reactor.util.concurrent.Queues;
public class ConsumerConnection<T> { public class ConsumerConnection<T> {
@ -61,20 +60,7 @@ public class ConsumerConnection<T> {
return remote.doOnError(ex -> { return remote.doOnError(ex -> {
synchronized (ConsumerConnection.this) { synchronized (ConsumerConnection.this) {
if (remoteCount <= 1) { if (remoteCount <= 1) {
if (remoteCount > 0 && localTerminationState == null) { onRemoteLastError(ex);
localTerminationState = Optional.of(ex);
if (LOG.isDebugEnabled()) {
LOG.debug("%s Local connection ended with failure".formatted(this.printStatus()), ex);
}
if (remoteCount <= 1) {
var sink = localTerminationSink;
reset();
sink.emitError(ex, EmitFailureHandler.FAIL_FAST);
if (LOG.isDebugEnabled()) {
LOG.debug("%s Local connection ended with failure, emitted termination failure".formatted(this.printStatus()));
}
}
}
} else { } else {
remoteCount--; remoteCount--;
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
@ -88,17 +74,7 @@ public class ConsumerConnection<T> {
synchronized (ConsumerConnection.this) { synchronized (ConsumerConnection.this) {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ending with status {}", this.printStatus(), s); if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ending with status {}", this.printStatus(), s);
if (remoteCount <= 1) { if (remoteCount <= 1) {
if (remoteCount > 0 && localTerminationState == null) { onLastFinally(s);
assert connectedState;
localTerminationState = Optional.empty();
if (s == SignalType.CANCEL) {
localTerminationSink.emitError(new CancelledChannelException(), EmitFailureHandler.FAIL_FAST);
} else {
localTerminationSink.emitEmpty(EmitFailureHandler.FAIL_FAST);
}
}
reset();
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, emitted termination complete", this.printStatus(), s);
} else { } else {
remoteCount--; remoteCount--;
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, but at least one remote is still online", this.printStatus(), s); if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, but at least one remote is still online", this.printStatus(), s);
@ -122,17 +98,39 @@ public class ConsumerConnection<T> {
}) })
.map(element -> new Timestamped<>(System.currentTimeMillis(), element)); .map(element -> new Timestamped<>(System.currentTimeMillis(), element));
} }
})).doFinally(s -> { })).doOnError(this::onRemoteLastError).doFinally(this::onLastFinally);
}
private synchronized void onLastFinally(SignalType s) {
if (remoteCount > 0 && localTerminationState == null) {
assert connectedState;
var ex = new CancelledChannelException();
localTerminationState = Optional.of(ex);
if (s == SignalType.CANCEL) { if (s == SignalType.CANCEL) {
synchronized (ConsumerConnection.this) { localTerminationSink.emitError(ex, EmitFailureHandler.FAIL_FAST);
local = null; } else {
var ex = new InterruptedException(); localTerminationSink.emitEmpty(EmitFailureHandler.FAIL_FAST);
localTerminationState = Optional.of(ex); }
if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus()); }
localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100))); reset();
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, emitted termination complete", this.printStatus(), s);
}
private synchronized void onRemoteLastError(Throwable ex) {
if (remoteCount > 0 && localTerminationState == null) {
localTerminationState = Optional.of(ex);
if (LOG.isDebugEnabled()) {
LOG.debug("%s Local connection ended with failure".formatted(this.printStatus()), ex);
}
if (remoteCount <= 1) {
var sink = localTerminationSink;
reset();
sink.emitError(ex, EmitFailureHandler.FAIL_FAST);
if (LOG.isDebugEnabled()) {
LOG.debug("%s Local connection ended with failure, emitted termination failure".formatted(this.printStatus()));
} }
} }
}); }
} }
public synchronized Mono<Void> connectRemote() { public synchronized Mono<Void> connectRemote() {
@ -154,7 +152,7 @@ public class ConsumerConnection<T> {
if (connectedState) { if (connectedState) {
if (localTerminationState == null) { if (localTerminationState == null) {
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus());
var ex = new InterruptedException(); var ex = new InterruptedException("Interrupted this connection because a new one is being prepared");
localTerminationState = Optional.of(ex); localTerminationState = Optional.of(ex);
localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100))); localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
@ -167,7 +165,7 @@ public class ConsumerConnection<T> {
} }
local = null; local = null;
remoteCount = 0; remoteCount = 0;
remotes.emitComplete(EmitFailureHandler.FAIL_FAST); remotes.tryEmitComplete();
remotes = Sinks.many().replay().all(); remotes = Sinks.many().replay().all();
connectedState = false; connectedState = false;
connectedSink = Sinks.empty(); connectedSink = Sinks.empty();
@ -179,7 +177,7 @@ public class ConsumerConnection<T> {
public synchronized void registerRemote(Flux<Payload> remote) { public synchronized void registerRemote(Flux<Payload> remote) {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus());
this.remoteCount++; this.remoteCount++;
this.remotes.emitNext(remote, EmitFailureHandler.FAIL_FAST); this.remotes.tryEmitNext(remote);
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
onChanged(); onChanged();
} }

View File

@ -56,8 +56,12 @@ public class ProducerConnection<T> {
return remoteTerminationSink.asMono().publishOn(Schedulers.parallel()); return remoteTerminationSink.asMono().publishOn(Schedulers.parallel());
} }
})).doFinally(s -> { })).doFinally(s -> {
if (s == SignalType.CANCEL) { if (s == SignalType.ON_ERROR || s == SignalType.CANCEL) {
synchronized (ProducerConnection.this) { synchronized (ProducerConnection.this) {
if (connectedState) {
connectedState = false;
connectedSink = Sinks.empty();
}
local = null; local = null;
if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus());
} }
@ -126,7 +130,7 @@ public class ProducerConnection<T> {
if (connectedState) { if (connectedState) {
if (remoteTerminationState == null) { if (remoteTerminationState == null) {
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus());
var ex = new InterruptedException(); var ex = new InterruptedException("Interrupted this connection because a new one is being prepared");
remoteTerminationState = Optional.of(ex); remoteTerminationState = Optional.of(ex);
remoteTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100))); remoteTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
@ -148,10 +152,6 @@ public class ProducerConnection<T> {
public synchronized void registerRemote() { public synchronized void registerRemote() {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus());
if (this.remoteCount > 0) {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote was already registered", this.printStatus());
throw new IllegalStateException("Remote is already registered");
}
this.remoteCount++; this.remoteCount++;
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus()); if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
onChanged(); onChanged();

View File

@ -22,6 +22,8 @@ public class RSocketUtils {
} }
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} finally {
payload.release();
} }
}); });
} }

View File

@ -7,12 +7,9 @@ import it.tdlight.reactiveapi.EventConsumer;
import it.tdlight.reactiveapi.EventProducer; import it.tdlight.reactiveapi.EventProducer;
import it.tdlight.reactiveapi.RSocketParameters; import it.tdlight.reactiveapi.RSocketParameters;
import it.tdlight.reactiveapi.Timestamped; import it.tdlight.reactiveapi.Timestamped;
import it.tdlight.reactiveapi.rsocket.CancelledChannelException;
import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -25,6 +22,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import reactor.core.Exceptions; import reactor.core.Exceptions;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -449,7 +447,7 @@ public abstract class TestChannel {
} }
@Test @Test
public void testFailTwoSubscribers() { public void testTwoSubscribers() {
var dataFlux = Flux.fromIterable(data).publish().autoConnect(); var dataFlux = Flux.fromIterable(data).publish().autoConnect();
var exRef1 = new AtomicReference<Throwable>(); var exRef1 = new AtomicReference<Throwable>();
var exRef2 = new AtomicReference<Throwable>(); var exRef2 = new AtomicReference<Throwable>();
@ -459,35 +457,43 @@ public abstract class TestChannel {
.retryWhen(Retry.fixedDelay(5, Duration.ofSeconds(1))) .retryWhen(Retry.fixedDelay(5, Duration.ofSeconds(1)))
.subscribe(n -> {}, ex -> exRef1.set(ex)); .subscribe(n -> {}, ex -> exRef1.set(ex));
Assertions.assertThrows(IllegalStateException.class, () -> { var exe = new Executable() {
try { @Override
Mono public void execute() throws Throwable {
.when(consumer try {
.consumeMessages() Mono
.limitRate(1) .when(consumer
.map(Timestamped::data) .consumeMessages()
.map(Integer::parseUnsignedInt) .limitRate(1)
.log("consumer-1", Level.INFO) .map(Timestamped::data)
.doOnError(ex -> exRef2.set(ex)), .map(Integer::parseUnsignedInt)
consumer .log("consumer-1", Level.INFO)
.consumeMessages() .doOnError(ex -> exRef2.set(ex)),
.limitRate(1) consumer
.map(Timestamped::data) .consumeMessages()
.map(Integer::parseUnsignedInt) .limitRate(1)
.log("consumer-2", Level.INFO) .map(Timestamped::data)
.onErrorResume(io.rsocket.exceptions.ApplicationErrorException.class, .map(Integer::parseUnsignedInt)
ex -> Mono.error(new IllegalStateException(ex)) .log("consumer-2", Level.INFO)
) .onErrorResume(io.rsocket.exceptions.ApplicationErrorException.class,
) ex -> Mono.error(new IllegalStateException(ex))
.block(); )
Assertions.assertNull(exRef1.get()); )
Assertions.assertNull(exRef2.get()); .block();
} catch (RuntimeException ex) { Assertions.assertNull(exRef1.get());
throw Exceptions.unwrap(ex); Assertions.assertNull(exRef2.get());
} finally { } catch (RuntimeException ex) {
eventProducer.dispose(); throw Exceptions.unwrap(ex);
} finally {
eventProducer.dispose();
}
} }
}); };
if (isConsumerClient()) {
Assertions.assertDoesNotThrow(exe);
} else {
Assertions.assertThrows(IllegalStateException.class, exe);
}
} }
@Test @Test