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) {
publisher.handleRequest(req.data());
} else {
LOG.error("Dropped request because no session is found: {}", req);
LOG.debug("Dropped request because no session is found: {}", req);
}
})
.subscribeOn(Schedulers.parallel())

View File

@ -67,6 +67,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink.OverflowStrategy;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks.EmitFailureHandler;
import reactor.core.publisher.Sinks.EmitResult;
import reactor.core.publisher.Sinks.Many;
import reactor.core.scheduler.Scheduler.Worker;
import reactor.core.scheduler.Schedulers;
@ -503,8 +504,21 @@ public abstract class ReactiveApiPublisher {
public void handleRequest(OnRequest<TdApi.Object> onRequestObj) {
handleRequestInternal(onRequestObj, response -> {
EmitResult status;
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
.subscribeOn(Schedulers.parallel())
.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();
return Flux.create(sink -> {
sink.onDispose(() -> {
disposable.dispose();
queue.close();
});
var startExVal = startEx.get();
if (startExVal != null) {
sink.error(startExVal);
return;
}
ref.set(sink);
sink.onCancel(() -> ref.set(null));
});

View File

@ -99,7 +99,9 @@ public class Stats extends Thread {
processedUpdatesRateSum += processedUpdatesRate;
clientBoundEventsRateSum += clientBoundEventsRate;
sentClientBoundEventsRateSum += sentClientBoundEventsRate;
out.append(String.format("%d:\t", clientIds.getLong(i)));
if (LOG.isTraceEnabled()) {
out.append(String.format("%d:\t", clientIds.getLong(i)));
}
} else {
receivedUpdatesRate = receivedUpdatesRateSum;
diskBuffered = diskBufferedSum;
@ -110,16 +112,18 @@ public class Stats extends Thread {
sentClientBoundEventsRate = sentClientBoundEventsRateSum;
out.append("Total:\t");
}
out.append(String.format(
"\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",
receivedUpdatesRate,
bufferedUpdatesRate,
ramBuffered,
diskBuffered,
processedUpdatesRate,
clientBoundEventsRate,
sentClientBoundEventsRate
));
if (i == currentClients || LOG.isTraceEnabled()) {
out.append(String.format(
"\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",
receivedUpdatesRate,
bufferedUpdatesRate,
ramBuffered,
diskBuffered,
processedUpdatesRate,
clientBoundEventsRate,
sentClientBoundEventsRate
));
}
}
out.append(String.format("%n"));

View File

@ -47,14 +47,13 @@ public class TdlibChannelsSharedHost implements Closeable {
private final TdlibChannelsServers tdServersChannels;
private final Disposable responsesSub;
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 Flux<Timestamped<OnRequest<Object>>> requests;
public TdlibChannelsSharedHost(Set<String> allLanes, TdlibChannelsServers tdServersChannels) {
this.tdServersChannels = tdServersChannels;
this.responsesSub = Mono.defer(() -> tdServersChannels.response()
.sendMessages(responses.asFlux()/*.log("responses", Level.FINE)*/))
this.responsesSub = tdServersChannels.response().sendMessages(responses.asFlux()/*.log("responses", Level.FINE)*/)
.repeatWhen(REPEAT_STRATEGY)
.retryWhen(RETRY_STRATEGY)
.subscribeOn(Schedulers.parallel())
@ -64,8 +63,7 @@ public class TdlibChannelsSharedHost implements Closeable {
Flux<ClientBoundEvent> outputEventsFlux = Flux
.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));
Mono.defer(() -> tdServersChannels.events(lane)
.sendMessages(outputEventsFlux))
tdServersChannels.events(lane).sendMessages(outputEventsFlux)
.repeatWhen(REPEAT_STRATEGY)
.retryWhen(RETRY_STRATEGY)
.subscribeOn(Schedulers.parallel())
@ -92,9 +90,9 @@ public class TdlibChannelsSharedHost implements Closeable {
if (eventsSink == null) {
throw new IllegalArgumentException("Lane " + lane + " does not exist");
}
eventsSink.emitNext(eventFlux.takeUntilOther(canceller.asMono()),
EmitFailureHandler.busyLooping(Duration.ofMillis(100))
);
synchronized (events) {
eventsSink.emitNext(eventFlux.takeUntilOther(canceller.asMono()), EmitFailureHandler.FAIL_FAST);
}
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.Many;
import reactor.core.scheduler.Schedulers;
import reactor.util.concurrent.Queues;
public class ConsumerConnection<T> {
@ -61,20 +60,7 @@ public class ConsumerConnection<T> {
return remote.doOnError(ex -> {
synchronized (ConsumerConnection.this) {
if (remoteCount <= 1) {
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()));
}
}
}
onRemoteLastError(ex);
} else {
remoteCount--;
if (LOG.isDebugEnabled()) {
@ -88,17 +74,7 @@ public class ConsumerConnection<T> {
synchronized (ConsumerConnection.this) {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ending with status {}", this.printStatus(), s);
if (remoteCount <= 1) {
if (remoteCount > 0 && localTerminationState == null) {
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);
onLastFinally(s);
} else {
remoteCount--;
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));
}
})).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) {
synchronized (ConsumerConnection.this) {
local = null;
var ex = new InterruptedException();
localTerminationState = Optional.of(ex);
if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus());
localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
localTerminationSink.emitError(ex, 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);
}
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() {
@ -154,7 +152,7 @@ public class ConsumerConnection<T> {
if (connectedState) {
if (localTerminationState == null) {
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);
localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
@ -167,7 +165,7 @@ public class ConsumerConnection<T> {
}
local = null;
remoteCount = 0;
remotes.emitComplete(EmitFailureHandler.FAIL_FAST);
remotes.tryEmitComplete();
remotes = Sinks.many().replay().all();
connectedState = false;
connectedSink = Sinks.empty();
@ -179,7 +177,7 @@ public class ConsumerConnection<T> {
public synchronized void registerRemote(Flux<Payload> remote) {
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus());
this.remoteCount++;
this.remotes.emitNext(remote, EmitFailureHandler.FAIL_FAST);
this.remotes.tryEmitNext(remote);
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
onChanged();
}

View File

@ -56,8 +56,12 @@ public class ProducerConnection<T> {
return remoteTerminationSink.asMono().publishOn(Schedulers.parallel());
}
})).doFinally(s -> {
if (s == SignalType.CANCEL) {
if (s == SignalType.ON_ERROR || s == SignalType.CANCEL) {
synchronized (ProducerConnection.this) {
if (connectedState) {
connectedState = false;
connectedSink = Sinks.empty();
}
local = null;
if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus());
}
@ -126,7 +130,7 @@ public class ProducerConnection<T> {
if (connectedState) {
if (remoteTerminationState == null) {
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);
remoteTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
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() {
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++;
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
onChanged();

View File

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