Flawless and resilient lifecycle management of tdlib instances
This commit is contained in:
parent
e9dd378765
commit
52cfeada84
2
pom.xml
2
pom.xml
@ -129,7 +129,7 @@
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java</artifactId>
|
||||
<version>[3.171.31,)</version>
|
||||
<version>[3.171.36,)</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.cavallium</groupId>
|
||||
|
@ -2,14 +2,24 @@ package it.tdlight.tdlibsession;
|
||||
|
||||
import io.vertx.core.eventbus.DeliveryOptions;
|
||||
import io.vertx.core.eventbus.EventBus;
|
||||
import io.vertx.core.eventbus.Message;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import io.vertx.core.eventbus.MessageConsumer;
|
||||
import io.vertx.core.eventbus.ReplyException;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class EventBusFlux {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EventBusFlux.class);
|
||||
@ -27,7 +37,11 @@ public class EventBusFlux {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> Mono<Void> serve(Flux<T> flux,
|
||||
/**
|
||||
*
|
||||
* @return tuple. T1 = flux served, T2 = error that caused cancelling of the subscription
|
||||
*/
|
||||
public static <T> Tuple2<Mono<Void>, Mono<Throwable>> serve(Flux<T> flux,
|
||||
EventBus eventBus,
|
||||
String fluxAddress,
|
||||
DeliveryOptions baseDeliveryOptions,
|
||||
@ -40,7 +54,8 @@ public class EventBusFlux {
|
||||
var signalDeliveryOptions = new DeliveryOptions(deliveryOptions)
|
||||
.setCodecName(signalsCodec.name());
|
||||
AtomicInteger subscriptionsCount = new AtomicInteger();
|
||||
return Mono.create(sink -> {
|
||||
One<Throwable> fatalErrorSink = Sinks.one();
|
||||
var servedMono = Mono.<Void>create(sink -> {
|
||||
MessageConsumer<byte[]> subscribe = eventBus.consumer(fluxAddress + ".subscribe");
|
||||
|
||||
subscribe.handler(msg -> {
|
||||
@ -62,47 +77,52 @@ public class EventBusFlux {
|
||||
subscriptionReady.<Long>handler(subscriptionReadyMsg -> {
|
||||
subscriptionReady.unregister(subscriptionReadyUnregistered -> {
|
||||
if (subscriptionReadyUnregistered.succeeded()) {
|
||||
var subscription = flux.subscribe(item -> {
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onNext(item), signalDeliveryOptions, msg2 -> {
|
||||
if (msg2.failed()) {
|
||||
logger.error("Failed to send onNext signal", msg2.cause());
|
||||
}
|
||||
});
|
||||
}, error -> {
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onError(error), signalDeliveryOptions, msg2 -> {
|
||||
logger.info("Errored flux \"" + fluxAddress + "\"");
|
||||
if (msg2.failed()) {
|
||||
logger.error("Failed to send onError signal", msg2.cause());
|
||||
}
|
||||
});
|
||||
}, () -> {
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onComplete(), signalDeliveryOptions, msg2 -> {
|
||||
logger.info("Completed flux \"" + fluxAddress + "\"");
|
||||
if (msg2.failed()) {
|
||||
logger.error("Failed to send onComplete signal", msg2.cause());
|
||||
}
|
||||
});
|
||||
});
|
||||
AtomicReference<Disposable> atomicSubscription = new AtomicReference<>(null);
|
||||
var subscription = flux
|
||||
.onErrorResume(error -> Mono
|
||||
.<Message<T>>create(errorSink -> {
|
||||
var responseHandler = MonoUtils.toHandler(errorSink);
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onError(error), signalDeliveryOptions, responseHandler);
|
||||
})
|
||||
.then(Mono.empty())
|
||||
)
|
||||
.flatMap(item -> Mono.<Message<T>>create(itemSink -> {
|
||||
var responseHandler = MonoUtils.toHandler(itemSink);
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onNext(item), signalDeliveryOptions, responseHandler);
|
||||
})).subscribe(response -> {}, error -> {
|
||||
if (error instanceof ReplyException) {
|
||||
var errorMessage = error.getMessage();
|
||||
if (errorMessage != null && errorMessage.contains("NO_HANDLERS")) {
|
||||
logger.error("Can't send a signal of flux \"" + fluxAddress + "\" because the connection was lost");
|
||||
} else {
|
||||
logger.error("Error when sending a signal of flux \"" + fluxAddress + "\": {}", error.getLocalizedMessage());
|
||||
}
|
||||
} else {
|
||||
logger.error("Error when sending a signal of flux \"" + fluxAddress + "\"", error);
|
||||
}
|
||||
fatalErrorSink.tryEmitValue(error);
|
||||
disposeFlux(atomicSubscription.get(), fatalErrorSink, cancel, dispose, fluxAddress, () -> {
|
||||
logger.warn("Forcefully disposed \"" + fluxAddress + "\" caused by the previous error");
|
||||
});
|
||||
}, () -> {
|
||||
eventBus.request(subscriptionAddress + ".signal", SignalMessage.<T>onComplete(), signalDeliveryOptions, msg2 -> {
|
||||
logger.info("Completed flux \"" + fluxAddress + "\"");
|
||||
if (msg2.failed()) {
|
||||
logger.error("Failed to send onComplete signal", msg2.cause());
|
||||
fatalErrorSink.tryEmitValue(msg2.cause());
|
||||
}
|
||||
});
|
||||
});
|
||||
atomicSubscription.set(subscription);
|
||||
|
||||
cancel.handler(msg3 -> {
|
||||
logger.warn("Cancelling flux \"" + fluxAddress + "\"");
|
||||
logger.trace("Cancelling flux \"" + fluxAddress + "\"");
|
||||
subscription.dispose();
|
||||
logger.debug("Cancelled flux \"" + fluxAddress + "\"");
|
||||
msg3.reply(EMPTY, deliveryOptions);
|
||||
});
|
||||
dispose.handler(msg2 -> {
|
||||
logger.warn("Disposing flux \"" + fluxAddress + "\"");
|
||||
subscription.dispose();
|
||||
cancel.unregister(v -> {
|
||||
if (v.failed()) {
|
||||
logger.error("Failed to unregister cancel", v.cause());
|
||||
}
|
||||
dispose.unregister(v2 -> {
|
||||
if (v.failed()) {
|
||||
logger.error("Failed to unregister dispose", v2.cause());
|
||||
}
|
||||
msg2.reply(EMPTY);
|
||||
});
|
||||
});
|
||||
disposeFlux(subscription, fatalErrorSink, cancel, dispose, fluxAddress, () -> msg2.reply(EMPTY));
|
||||
});
|
||||
|
||||
cancel.completionHandler(h -> {
|
||||
@ -148,6 +168,33 @@ public class EventBusFlux {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Tuples.of(servedMono, fatalErrorSink.asMono());
|
||||
}
|
||||
|
||||
private static void disposeFlux(@Nullable Disposable subscription,
|
||||
One<Throwable> fatalErrorSink,
|
||||
MessageConsumer<byte[]> cancel,
|
||||
MessageConsumer<byte[]> dispose,
|
||||
String fluxAddress,
|
||||
Runnable after) {
|
||||
logger.trace("Disposing flux \"" + fluxAddress + "\"");
|
||||
fatalErrorSink.tryEmitEmpty();
|
||||
if (subscription != null) {
|
||||
subscription.dispose();
|
||||
}
|
||||
cancel.unregister(v -> {
|
||||
if (v.failed()) {
|
||||
logger.error("Failed to unregister cancel", v.cause());
|
||||
}
|
||||
dispose.unregister(v2 -> {
|
||||
if (v.failed()) {
|
||||
logger.error("Failed to unregister dispose", v2.cause());
|
||||
}
|
||||
logger.debug("Disposed flux \"" + fluxAddress + "\"");
|
||||
after.run();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> Flux<T> connect(EventBus eventBus,
|
||||
|
@ -3,6 +3,7 @@ package it.tdlight.tdlibsession.remoteclient;
|
||||
import io.vertx.core.AsyncResult;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.eventbus.Message;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.core.shareddata.AsyncMap;
|
||||
@ -17,6 +18,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -37,6 +39,7 @@ public class TDLibRemoteClient implements AutoCloseable {
|
||||
private final Set<String> membersAddresses;
|
||||
private final Many<TdClusterManager> clusterManager = Sinks.many().replay().latest();
|
||||
private final Scheduler deploymentScheduler = Schedulers.newSingle("TDLib", false);
|
||||
private final AtomicInteger statsActiveDeployments = new AtomicInteger();
|
||||
|
||||
public TDLibRemoteClient(SecurityInfo securityInfo, String masterHostname, String netInterface, int port, Set<String> membersAddresses) {
|
||||
this.securityInfo = securityInfo;
|
||||
@ -124,7 +127,7 @@ public class TDLibRemoteClient implements AutoCloseable {
|
||||
if (mapResult.succeeded()) {
|
||||
var deployableBotAddresses = mapResult.result();
|
||||
|
||||
clusterManager.getSharedData().getLockWithTimeout("deployment", 15000, lockAcquisitionResult -> {
|
||||
clusterManager.getSharedData().getLock("deployment", lockAcquisitionResult -> {
|
||||
if (lockAcquisitionResult.succeeded()) {
|
||||
var deploymentLock = lockAcquisitionResult.result();
|
||||
putAllAsync(deployableBotAddresses, botAddresses.values(), (AsyncResult<Void> putAllResult) -> {
|
||||
@ -168,7 +171,7 @@ public class TDLibRemoteClient implements AutoCloseable {
|
||||
AsyncMap<Object, Object> deployableBotAddresses) {
|
||||
clusterManager.getEventBus().consumer("tdlib.remoteclient.clients.deploy", (Message<String> msg) -> {
|
||||
|
||||
clusterManager.getSharedData().getLockWithTimeout("deployment", 15000, lockAcquisitionResult -> {
|
||||
clusterManager.getSharedData().getLock("deployment", lockAcquisitionResult -> {
|
||||
if (lockAcquisitionResult.succeeded()) {
|
||||
var deploymentLock = lockAcquisitionResult.result();
|
||||
var botAddress = msg.body();
|
||||
@ -229,14 +232,23 @@ public class TDLibRemoteClient implements AutoCloseable {
|
||||
}
|
||||
|
||||
private void deployBot(TdClusterManager clusterManager, String botAddress, Handler<AsyncResult<String>> deploymentHandler) {
|
||||
logger.info("Active deployments: " + statsActiveDeployments.incrementAndGet());
|
||||
AsyncTdMiddleEventBusServer verticle = new AsyncTdMiddleEventBusServer(clusterManager);
|
||||
verticle.onBeforeStop(handler -> {
|
||||
clusterManager.getSharedData().getLockWithTimeout("deployment", 30000, lockAcquisitionResult -> {
|
||||
var afterStopPromise = Promise.promise();
|
||||
if (verticle.onAfterStop(handler -> {
|
||||
afterStopPromise.complete();
|
||||
handler.complete();
|
||||
}, true).isFailure()) {
|
||||
deploymentHandler.handle(Future.failedFuture(new IllegalStateException("Failed to register to event onAfterStop")));
|
||||
return;
|
||||
}
|
||||
if (verticle.onBeforeStop(handler -> {
|
||||
clusterManager.getSharedData().getLock("deployment", lockAcquisitionResult -> {
|
||||
if (lockAcquisitionResult.succeeded()) {
|
||||
var deploymentLock = lockAcquisitionResult.result();
|
||||
verticle.onAfterStop(handler2 -> {
|
||||
afterStopPromise.future().onComplete(handler2 -> {
|
||||
deploymentLock.release();
|
||||
handler2.complete();
|
||||
logger.info("Active deployments: " + statsActiveDeployments.decrementAndGet());
|
||||
});
|
||||
clusterManager.getSharedData().getClusterWideMap("runningBotAddresses", (AsyncResult<AsyncMap<String, String>> mapResult) -> {
|
||||
if (mapResult.succeeded()) {
|
||||
@ -260,7 +272,10 @@ public class TDLibRemoteClient implements AutoCloseable {
|
||||
handler.fail(lockAcquisitionResult.cause());
|
||||
}
|
||||
});
|
||||
});
|
||||
}, false).isFailure()) {
|
||||
deploymentHandler.handle(Future.failedFuture(new IllegalStateException("Failed to register to event onBeforeStop")));
|
||||
return;
|
||||
}
|
||||
verticle.start(botAddress, botAddress, false).doOnError(error -> {
|
||||
logger.error("Can't deploy bot \"" + botAddress + "\"", error);
|
||||
}).subscribeOn(deploymentScheduler).subscribe(v -> {}, err -> {
|
||||
|
@ -10,7 +10,6 @@ import it.tdlight.jni.TdApi.Ok;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlight.ClientManager;
|
||||
import java.time.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
@ -36,7 +35,7 @@ public class AsyncTdDirectImpl implements AsyncTdDirect {
|
||||
@Override
|
||||
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function request, boolean synchronous) {
|
||||
if (synchronous) {
|
||||
return td.asMono().flatMap(td -> Mono.fromCallable(() -> {
|
||||
return td.asMono().single().flatMap(td -> Mono.fromCallable(() -> {
|
||||
if (td != null) {
|
||||
return TdResult.<T>of(td.execute(request));
|
||||
} else {
|
||||
@ -47,18 +46,17 @@ public class AsyncTdDirectImpl implements AsyncTdDirect {
|
||||
}
|
||||
}).publishOn(Schedulers.boundedElastic()).single()).subscribeOn(tdScheduler);
|
||||
} else {
|
||||
return td.asMono().flatMap(td -> Mono.<TdResult<T>>create(sink -> {
|
||||
return td.asMono().single().flatMap(td -> Mono.<TdResult<T>>create(sink -> {
|
||||
if (td != null) {
|
||||
try {
|
||||
td.send(request, v -> sink.success(TdResult.of(v)), sink::error);
|
||||
} catch (Throwable t) {
|
||||
sink.error(t);
|
||||
}
|
||||
td.send(request, v -> sink.success(TdResult.of(v)), sink::error);
|
||||
} else {
|
||||
if (request.getConstructor() == Close.CONSTRUCTOR) {
|
||||
logger.trace("Sending close success to sink " + sink.toString());
|
||||
sink.success(TdResult.<T>of(new Ok()));
|
||||
} else {
|
||||
logger.trace("Sending close error to sink " + sink.toString());
|
||||
sink.error(new IllegalStateException("TDLib client is destroyed"));
|
||||
}
|
||||
sink.error(new IllegalStateException("TDLib client is destroyed"));
|
||||
}
|
||||
})).single().subscribeOn(tdScheduler);
|
||||
}
|
||||
@ -66,15 +64,17 @@ public class AsyncTdDirectImpl implements AsyncTdDirect {
|
||||
|
||||
@Override
|
||||
public Flux<TdResult<TdApi.Object>> receive(AsyncTdDirectOptions options) {
|
||||
// If closed it will be either true or false
|
||||
final One<Boolean> closedFromTd = Sinks.one();
|
||||
return Flux.<TdResult<TdApi.Object>>create(emitter -> {
|
||||
One<java.lang.Object> closedFromTd = Sinks.one();
|
||||
var client = ClientManager.create((Object object) -> {
|
||||
emitter.next(TdResult.of(object));
|
||||
// Close the emitter if receive closed state
|
||||
if (object.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR
|
||||
&& ((UpdateAuthorizationState) object).authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
closedFromTd.tryEmitValue(new java.lang.Object());
|
||||
logger.debug("Received closed status from tdlib");
|
||||
closedFromTd.tryEmitValue(true);
|
||||
emitter.complete();
|
||||
}
|
||||
}, emitter::error, emitter::error);
|
||||
@ -86,10 +86,21 @@ public class AsyncTdDirectImpl implements AsyncTdDirect {
|
||||
|
||||
// Send close if the stream is disposed before tdlib is closed
|
||||
emitter.onDispose(() -> {
|
||||
closedFromTd.asMono().take(Duration.ofMillis(10)).switchIfEmpty(Mono.fromRunnable(() -> client.send(new Close(),
|
||||
result -> logger.warn("Close result: {}", result),
|
||||
ex -> logger.error("Error when disposing td client", ex)
|
||||
))).subscribeOn(tdScheduler).subscribe();
|
||||
// Try to emit false, so that if it has not been closed from tdlib, now it is explicitly false.
|
||||
closedFromTd.tryEmitValue(false);
|
||||
|
||||
closedFromTd.asMono()
|
||||
.doOnNext(isClosedFromTd -> {
|
||||
if (!isClosedFromTd) {
|
||||
logger.warn("The stream has been disposed without closing tdlib. Sending TdApi.Close()...");
|
||||
client.send(new Close(),
|
||||
result -> logger.warn("Close result: {}", result),
|
||||
ex -> logger.error("Error when disposing td client", ex)
|
||||
);
|
||||
}
|
||||
})
|
||||
.subscribeOn(tdScheduler)
|
||||
.subscribe();
|
||||
});
|
||||
}).subscribeOn(tdScheduler);
|
||||
}
|
||||
|
@ -66,25 +66,33 @@ public class AsyncTdEasy {
|
||||
private final One<FatalErrorType> fatalError = Sinks.one();
|
||||
private final AsyncTdMiddle td;
|
||||
private final String logName;
|
||||
private final Flux<Update> incomingUpdatesCo;
|
||||
private final Flux<Update> incomingUpdates;
|
||||
|
||||
public AsyncTdEasy(AsyncTdMiddle td, String logName) {
|
||||
this.td = td;
|
||||
this.logName = logName;
|
||||
|
||||
// todo: use Duration.ZERO instead of 10ms interval
|
||||
this.incomingUpdatesCo = td.receive()
|
||||
.filterWhen(update -> Mono.from(requestedDefinitiveExit).map(requestedDefinitiveExit -> !requestedDefinitiveExit))
|
||||
this.incomingUpdates = td.receive()
|
||||
.flatMap(this::preprocessUpdates)
|
||||
.flatMap(update -> Mono.from(this.getState()).single().map(state -> new AsyncTdUpdateObj(state, update)))
|
||||
.filter(upd -> upd.getState().getConstructor() == AuthorizationStateReady.CONSTRUCTOR)
|
||||
.map(upd -> (TdApi.Update) upd.getUpdate())
|
||||
.doOnError(ex -> {
|
||||
logger.error(ex.getLocalizedMessage(), ex);
|
||||
}).doOnNext(v -> {
|
||||
if (logger.isDebugEnabled()) logger.debug(v.toString());
|
||||
}).doOnComplete(() -> {
|
||||
authState.onNext(new AuthorizationStateClosed());
|
||||
authState.asFlux().take(1).single().subscribe(authState -> {
|
||||
if (authState.getConstructor() != AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.warn("Updates stream has closed while"
|
||||
+ " the current authorization state is"
|
||||
+ " still {}. Setting authorization state as closed!", authState.getClass().getSimpleName());
|
||||
this.authState.onNext(new AuthorizationStateClosed());
|
||||
}
|
||||
});
|
||||
})
|
||||
.doOnTerminate(() -> {
|
||||
logger.debug("Incoming updates flux terminated. Setting requestedDefinitiveExit: true");
|
||||
requestedDefinitiveExit.onNext(true);
|
||||
})
|
||||
.subscribeOn(scheduler)
|
||||
.publish().refCount(1);
|
||||
@ -130,7 +138,7 @@ public class AsyncTdEasy {
|
||||
}
|
||||
|
||||
private Flux<TdApi.Update> getIncomingUpdates(boolean includePreAuthUpdates) {
|
||||
return Flux.from(incomingUpdatesCo).doFinally(s -> requestedDefinitiveExit.onNext(true)).subscribeOn(scheduler);
|
||||
return incomingUpdates.subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -291,19 +299,27 @@ public class AsyncTdEasy {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.then(Mono.from(requestedDefinitiveExit).single())
|
||||
.then(requestedDefinitiveExit.asFlux().take(1).single())
|
||||
.filter(closeRequested -> !closeRequested)
|
||||
.doOnSuccess(v -> requestedDefinitiveExit.onNext(true))
|
||||
.then(td.execute(new TdApi.Close(), false))
|
||||
.doOnNext(ok -> {
|
||||
logger.debug("Received Ok after TdApi.Close");
|
||||
.doOnSuccess(s -> {
|
||||
logger.debug("Setting requestedDefinitiveExit: true");
|
||||
requestedDefinitiveExit.onNext(true);
|
||||
})
|
||||
.then(td.execute(new TdApi.Close(), false).doOnSubscribe(s -> {
|
||||
logger.debug("Sending TdApi.Close");
|
||||
}))
|
||||
.doOnNext(closeResponse -> logger.debug("TdApi.Close response is: \"{}\"",
|
||||
closeResponse.toString().replace('\n', ' ')
|
||||
))
|
||||
.then(authState
|
||||
.filter(authorizationState -> authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR)
|
||||
.take(1)
|
||||
.singleOrEmpty())
|
||||
.doOnNext(ok -> {
|
||||
logger.info("Received AuthorizationStateClosed after TdApi.Close");
|
||||
logger.debug("Received AuthorizationStateClosed after TdApi.Close");
|
||||
})
|
||||
.doOnSuccess(s -> {
|
||||
logger.info("AsyncTdEasy closed successfully");
|
||||
})
|
||||
.then()
|
||||
.subscribeOn(scheduler);
|
||||
@ -495,14 +511,18 @@ public class AsyncTdEasy {
|
||||
this.authState.onNext(new AuthorizationStateReady());
|
||||
return Mono.empty();
|
||||
}
|
||||
case AuthorizationStateClosing.CONSTRUCTOR:
|
||||
logger.debug("Received AuthorizationStateClosing from td");
|
||||
return Mono.empty();
|
||||
case AuthorizationStateClosed.CONSTRUCTOR:
|
||||
logger.debug("Received AuthorizationStateClosed from td");
|
||||
return Mono.from(requestedDefinitiveExit).doOnNext(closeRequested -> {
|
||||
if (closeRequested) {
|
||||
logger.info("AsyncTdEasy closed successfully");
|
||||
logger.debug("td closed successfully");
|
||||
} else {
|
||||
logger.warn("AsyncTdEasy closed unexpectedly: " + logName);
|
||||
authState.onNext(obj);
|
||||
logger.warn("td closed unexpectedly: {}", logName);
|
||||
}
|
||||
authState.onNext(obj);
|
||||
}).flatMap(closeRequested -> {
|
||||
if (closeRequested) {
|
||||
return Mono
|
||||
|
@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.warp.commonutils.error.InitializationException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
|
||||
@ -47,6 +48,7 @@ public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements Asy
|
||||
public static final boolean OUTPUT_REQUESTS = false;
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
|
||||
private final Many<Boolean> tdCloseRequested = Sinks.many().replay().latestOrDefault(false);
|
||||
private final Many<Boolean> tdClosed = Sinks.many().replay().latestOrDefault(false);
|
||||
private final DeliveryOptions deliveryOptions;
|
||||
private final DeliveryOptions deliveryOptionsWithTimeout;
|
||||
@ -172,9 +174,30 @@ public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements Asy
|
||||
|
||||
@Override
|
||||
public void stop(Promise<Void> stopPromise) {
|
||||
logger.debug("Stopping AsyncTdMiddle client verticle...");
|
||||
readyToStartConsumer.unregister(result -> {
|
||||
tdClosed.tryEmitNext(true);
|
||||
stopPromise.complete();
|
||||
if (result.failed()) {
|
||||
logger.error("Failed to unregister readyToStart consumer");
|
||||
} else {
|
||||
logger.debug("Unregistered readyToStart consumer");
|
||||
}
|
||||
tdCloseRequested.asFlux().take(1).single().flatMap(closeRequested -> {
|
||||
if (!closeRequested) {
|
||||
logger.warn("Verticle is being stopped before closing TDLib with Close()! Sending Close() before stopping...");
|
||||
return this.execute(new TdApi.Close(), false).doOnTerminate(() -> {
|
||||
logger.debug("Close() sent to td");
|
||||
markCloseRequested();
|
||||
}).then();
|
||||
} else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}).thenMany(tdClosed.asFlux()).filter(closed -> closed).take(1).subscribe(v -> {}, cause -> {
|
||||
logger.debug("Failed to stop AsyncTdMiddle client verticle");
|
||||
stopPromise.fail(cause);
|
||||
}, () -> {
|
||||
logger.debug("Stopped AsyncTdMiddle client verticle");
|
||||
stopPromise.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -204,7 +227,7 @@ public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements Asy
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
var fluxCodec = new TdResultListMessageCodec();
|
||||
return Mono.from(tdClosed.asFlux()).single().filter(tdClosed -> !tdClosed).flatMapMany(_closed -> EventBusFlux
|
||||
return tdCloseRequested.asFlux().take(1).single().filter(close -> !close).flatMapMany(_closed -> EventBusFlux
|
||||
.<TdResultList>connect(cluster.getEventBus(),
|
||||
botAddress + ".updates",
|
||||
deliveryOptions,
|
||||
@ -235,12 +258,38 @@ public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements Asy
|
||||
if (item.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var state = (UpdateAuthorizationState) item;
|
||||
if (state.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
|
||||
// Send tdClosed early to avoid errors
|
||||
tdClosed.tryEmitNext(true);
|
||||
logger.debug("Received AuthorizationStateClosed from td. Marking td as closed");
|
||||
markCloseRequested();
|
||||
markClosed();
|
||||
}
|
||||
}
|
||||
}));
|
||||
})).doFinally(s -> {
|
||||
if (s == SignalType.ON_ERROR) {
|
||||
// Send tdClosed early to avoid errors
|
||||
logger.debug("Updates flux terminated with an error signal. Marking td as closed");
|
||||
markCloseRequested();
|
||||
markClosed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void markCloseRequested() {
|
||||
if (tdCloseRequested.tryEmitNext(true).isFailure()) {
|
||||
logger.error("Failed to set tdCloseRequested");
|
||||
if (tdCloseRequested.tryEmitComplete().isFailure()) {
|
||||
logger.error("Failed to complete tdCloseRequested");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markClosed() {
|
||||
if (tdClosed.tryEmitNext(true).isFailure()) {
|
||||
logger.error("Failed to set tdClosed");
|
||||
if (tdClosed.tryEmitComplete().isFailure()) {
|
||||
logger.error("Failed to complete tdClosed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -255,7 +304,7 @@ public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements Asy
|
||||
.replace(" = ", "="));
|
||||
}
|
||||
|
||||
return Mono.from(tdClosed.asFlux()).single().filter(tdClosed -> !tdClosed).<TdResult<T>>flatMap((_x) -> Mono.create(sink -> {
|
||||
return tdCloseRequested.asFlux().take(1).single().filter(close -> !close).<TdResult<T>>flatMap((_x) -> Mono.create(sink -> {
|
||||
try {
|
||||
cluster
|
||||
.getEventBus()
|
||||
|
@ -37,7 +37,7 @@ public class AsyncTdMiddleLocal implements AsyncTdMiddle {
|
||||
try {
|
||||
return AsyncTdMiddleEventBusClient.getAndDeployInstance(masterClusterManager, botAlias, botAddress, true).doOnNext(cli -> {
|
||||
this.cli.onNext(cli);
|
||||
}).doOnError(error -> this.cli.onError(error)).doFinally(_v -> this.cli.onComplete());
|
||||
}).doOnError(error -> this.cli.onError(error)).doOnSuccess(_v -> this.cli.onComplete());
|
||||
} catch (InitializationException e) {
|
||||
this.cli.onError(e);
|
||||
return Mono.error(e);
|
||||
|
@ -10,6 +10,10 @@ import io.vertx.core.eventbus.Message;
|
||||
import io.vertx.core.eventbus.MessageConsumer;
|
||||
import it.tdlight.common.ConstructorDetector;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.jni.TdApi.Update;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.EventBusFlux;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.TdResultMessage;
|
||||
@ -27,39 +31,41 @@ import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitResult;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class AsyncTdMiddleEventBusServer {
|
||||
|
||||
// Static values
|
||||
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusServer.class);
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
public static final Duration WAIT_DURATION = Duration.ofSeconds(1);
|
||||
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
// todo: restore duration to 2 seconds instead of 10 millis, when the bug of tdlight double queue wait is fixed
|
||||
public static final Duration WAIT_DURATION = Duration.ofSeconds(1);// Duration.ofMillis(10);
|
||||
// If you enable this the poll will wait up to 100 additional milliseconds between each poll, if the server is remote
|
||||
private static final boolean ENABLE_MINIMUM_POLL_WAIT_INTERVAL = false;
|
||||
|
||||
// Values configured from constructor
|
||||
private final TdClusterManager cluster;
|
||||
private final AsyncTdDirectOptions tdOptions;
|
||||
|
||||
private String botAlias;
|
||||
private String botAddress;
|
||||
private boolean local;
|
||||
// Variables configured by the user at startup
|
||||
private final One<String> botAlias = Sinks.one();
|
||||
private final One<String> botAddress = Sinks.one();
|
||||
private final One<Boolean> local = Sinks.one();
|
||||
private final Many<Consumer<Promise<Void>>> onBeforeStopListeners = Sinks.many().replay().all();
|
||||
private final Many<Consumer<Promise<Void>>> onAfterStopListeners = Sinks.many().replay().all();
|
||||
|
||||
protected AsyncTdDirectImpl td;
|
||||
/**
|
||||
* Value is not important, emits when a request is received
|
||||
*/
|
||||
private final List<Consumer<Promise<Void>>> onBeforeStopListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<Consumer<Promise<Void>>> onAfterStopListeners = new CopyOnWriteArrayList<>();
|
||||
private MessageConsumer<?> startConsumer;
|
||||
private MessageConsumer<byte[]> isWorkingConsumer;
|
||||
private MessageConsumer<ExecuteObject> executeConsumer;
|
||||
// Variables configured at startup
|
||||
private final One<AsyncTdDirectImpl> td = Sinks.one();
|
||||
private final One<MessageConsumer<byte[]>> startConsumer = Sinks.one();
|
||||
private final One<MessageConsumer<byte[]>> isWorkingConsumer = Sinks.one();
|
||||
private final One<MessageConsumer<ExecuteObject>> executeConsumer = Sinks.one();
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public AsyncTdMiddleEventBusServer(TdClusterManager clusterManager) {
|
||||
@ -74,60 +80,139 @@ public class AsyncTdMiddleEventBusServer {
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<Void> start(String botAddress, String botAlias, boolean local) {
|
||||
public Mono<AsyncTdMiddleEventBusServer> start(String botAddress, String botAlias, boolean local) {
|
||||
return Mono.<Void>create(sink -> {
|
||||
if (botAddress == null || botAddress.isEmpty()) {
|
||||
sink.error(new IllegalArgumentException("botAddress is not set!"));
|
||||
return;
|
||||
}
|
||||
if (this.botAddress.tryEmitValue(botAddress).isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to set botAddress"));
|
||||
return;
|
||||
}
|
||||
this.botAddress = botAddress;
|
||||
if (botAlias == null || botAlias.isEmpty()) {
|
||||
sink.error(new IllegalArgumentException("botAlias is not set!"));
|
||||
return;
|
||||
}
|
||||
if (this.botAlias.tryEmitValue(botAlias).isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to set botAlias"));
|
||||
return;
|
||||
}
|
||||
if (this.local.tryEmitValue(local).isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to set local"));
|
||||
return;
|
||||
}
|
||||
var td = new AsyncTdDirectImpl(botAlias);
|
||||
if (this.td.tryEmitValue(td).isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to set td instance"));
|
||||
return;
|
||||
}
|
||||
if (this.onBeforeStopListeners.tryEmitComplete().isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to finalize \"before stop\" listeners"));
|
||||
return;
|
||||
}
|
||||
if (this.onAfterStopListeners.tryEmitComplete().isFailure()) {
|
||||
sink.error(new IllegalStateException("Failed to finalize \"after stop\" listeners"));
|
||||
return;
|
||||
}
|
||||
this.botAlias = botAlias;
|
||||
this.local = local;
|
||||
this.td = new AsyncTdDirectImpl(botAlias);
|
||||
|
||||
AtomicBoolean alreadyDeployed = new AtomicBoolean(false);
|
||||
this.startConsumer = cluster.getEventBus().consumer(botAddress + ".start", (Message<byte[]> msg) -> {
|
||||
if (alreadyDeployed.compareAndSet(false, true)) {
|
||||
this.listen().then(this.pipe()).then(Mono.<Void>create(registrationSink -> {
|
||||
this.isWorkingConsumer = cluster.getEventBus().consumer(botAddress + ".isWorking", (Message<byte[]> workingMsg) -> {
|
||||
workingMsg.reply(EMPTY, cluster.newDeliveryOpts().setLocalOnly(local));
|
||||
var startConsumer = cluster.getEventBus().<byte[]>consumer(botAddress + ".start");
|
||||
if (this.startConsumer.tryEmitValue(startConsumer).isSuccess()) {
|
||||
startConsumer.handler((Message<byte[]> msg) -> {
|
||||
if (alreadyDeployed.compareAndSet(false, true)) {
|
||||
startConsumer.unregister(startConsumerUnregistered -> {
|
||||
if (startConsumerUnregistered.succeeded()) {
|
||||
onSuccessfulStartRequest(msg, td, botAddress, botAlias, local);
|
||||
} else {
|
||||
logger.error("Failed to unregister start consumer");
|
||||
}
|
||||
});
|
||||
this.isWorkingConsumer.completionHandler(MonoUtils.toHandler(registrationSink));
|
||||
}))
|
||||
.subscribe(v -> {}, ex -> {
|
||||
logger.info(botAddress + " server deployed and started. succeeded: false");
|
||||
logger.error(ex.getLocalizedMessage(), ex);
|
||||
msg.fail(500, ex.getLocalizedMessage());
|
||||
}, () -> {
|
||||
logger.info(botAddress + " server deployed and started. succeeded: true");
|
||||
msg.reply(EMPTY);
|
||||
});
|
||||
} else {
|
||||
msg.reply(EMPTY);
|
||||
}
|
||||
});
|
||||
startConsumer.completionHandler(h -> {
|
||||
logger.info(botAddress + " server deployed. succeeded: " + h.succeeded());
|
||||
if (h.succeeded()) {
|
||||
logger.debug("Sending " + botAddress + ".readyToStart");
|
||||
cluster.getEventBus().request(botAddress + ".readyToStart", EMPTY, cluster.newDeliveryOpts().setSendTimeout(30000), msg -> {
|
||||
sink.success();
|
||||
});
|
||||
} else {
|
||||
sink.error(h.cause());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
msg.reply(EMPTY);
|
||||
}
|
||||
});
|
||||
startConsumer.completionHandler(h -> {
|
||||
logger.info(botAddress + " server deployed. succeeded: " + h.succeeded());
|
||||
if (h.succeeded()) {
|
||||
var readyToStartOpts = cluster.newDeliveryOpts().setSendTimeout(30000);
|
||||
logger.debug("Sending " + botAddress + ".readyToStart");
|
||||
cluster.getEventBus().request(botAddress + ".readyToStart", EMPTY, readyToStartOpts, msg -> sink.success());
|
||||
} else {
|
||||
sink.error(h.cause());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sink.error(new IllegalStateException("Failed to set startConsumer"));
|
||||
}
|
||||
}).thenReturn(this);
|
||||
}
|
||||
|
||||
private void onSuccessfulStartRequest(Message<byte[]> msg, AsyncTdDirectImpl td, String botAddress, String botAlias, boolean local) {
|
||||
this.listen(td, botAddress, botAlias, local).then(this.pipe(td, botAddress, botAlias, local)).then(Mono.<Void>create(registrationSink -> {
|
||||
var isWorkingConsumer = cluster.getEventBus().<byte[]>consumer(botAddress + ".isWorking");
|
||||
if (this.isWorkingConsumer.tryEmitValue(isWorkingConsumer).isSuccess()) {
|
||||
isWorkingConsumer.handler((Message<byte[]> workingMsg) -> {
|
||||
workingMsg.reply(EMPTY, cluster.newDeliveryOpts().setLocalOnly(local));
|
||||
});
|
||||
isWorkingConsumer.completionHandler(MonoUtils.toHandler(registrationSink));
|
||||
} else {
|
||||
logger.error("Failed to set isWorkingConsumer");
|
||||
msg.fail(500, "Failed to set isWorkingConsumer");
|
||||
}
|
||||
})).subscribe(v -> {}, ex -> {
|
||||
logger.error("Deploy and start of bot \"" + botAlias + "\": ❌ Failed", ex);
|
||||
msg.fail(500, ex.getLocalizedMessage());
|
||||
}, () -> {
|
||||
logger.info("Deploy and start of bot \"" + botAlias + "\": ✅ Succeeded");
|
||||
msg.reply(EMPTY);
|
||||
});
|
||||
}
|
||||
|
||||
public void onBeforeStop(Consumer<Promise<Void>> r) {
|
||||
this.onBeforeStopListeners.add(r);
|
||||
/**
|
||||
* Register a before stop listener
|
||||
* @param eventListener listener
|
||||
* @return success if the listener has been registered correctly
|
||||
*/
|
||||
public EmitResult onBeforeStop(Consumer<Promise<Void>> eventListener, boolean crashOnFail) {
|
||||
if (crashOnFail) {
|
||||
return this.onBeforeStopListeners.tryEmitNext(eventListener);
|
||||
} else {
|
||||
return this.onBeforeStopListeners.tryEmitNext(promise -> {
|
||||
Promise<Void> falliblePromise = Promise.promise();
|
||||
falliblePromise.future().onComplete(result -> {
|
||||
if (result.failed()) {
|
||||
logger.warn("A beforeStop listener failed. Ignored error", result.cause());
|
||||
}
|
||||
promise.complete();
|
||||
});
|
||||
eventListener.accept(falliblePromise);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void onAfterStop(Consumer<Promise<Void>> r) {
|
||||
this.onAfterStopListeners.add(r);
|
||||
/**
|
||||
* Register an after stop listener
|
||||
* @param eventListener listener
|
||||
* @return success if the listener has been registered correctly
|
||||
*/
|
||||
public EmitResult onAfterStop(Consumer<Promise<Void>> eventListener, boolean crashOnFail) {
|
||||
if (crashOnFail) {
|
||||
return this.onAfterStopListeners.tryEmitNext(eventListener);
|
||||
} else {
|
||||
return this.onAfterStopListeners.tryEmitNext(promise -> {
|
||||
Promise<Void> falliblePromise = Promise.promise();
|
||||
falliblePromise.future().onComplete(result -> {
|
||||
if (result.failed()) {
|
||||
logger.warn("An afterStop listener failed. Ignored error", result.cause());
|
||||
}
|
||||
promise.complete();
|
||||
});
|
||||
eventListener.accept(falliblePromise);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void runAll(List<Consumer<Promise<Void>>> actions, Handler<AsyncResult<Void>> resultHandler) {
|
||||
@ -147,115 +232,160 @@ public class AsyncTdMiddleEventBusServer {
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Void> listen() {
|
||||
private Mono<Void> listen(AsyncTdDirectImpl td, String botAddress, String botAlias, boolean local) {
|
||||
return Mono.<Void>create(registrationSink -> {
|
||||
MessageConsumer<ExecuteObject> executeConsumer = cluster.getEventBus().consumer(botAddress + ".execute");
|
||||
if (this.executeConsumer.tryEmitValue(executeConsumer).isFailure()) {
|
||||
registrationSink.error(new IllegalStateException("Failed to set executeConsumer"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.executeConsumer = cluster.getEventBus().<ExecuteObject>consumer(botAddress + ".execute", (Message<ExecuteObject> msg) -> {
|
||||
try {
|
||||
Flux.<Message<ExecuteObject>>create(sink -> {
|
||||
executeConsumer.handler(sink::next);
|
||||
executeConsumer.completionHandler(MonoUtils.toHandler(registrationSink));
|
||||
})
|
||||
.doOnNext(msg -> {
|
||||
if (OUTPUT_REQUESTS) {
|
||||
System.out.println(":=> " + msg
|
||||
.body()
|
||||
.getRequest()
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
.replace("\t", "")
|
||||
.replace(" ", "")
|
||||
.replace(" = ", "="));
|
||||
}
|
||||
})
|
||||
.flatMap(msg -> td
|
||||
.execute(msg.body().getRequest(), msg.body().isExecuteDirectly())
|
||||
.map(result -> Tuples.of(msg, result)))
|
||||
.handle((tuple, sink) -> {
|
||||
var msg = tuple.getT1();
|
||||
var response = tuple.getT2();
|
||||
var replyOpts = cluster.newDeliveryOpts().setLocalOnly(local);
|
||||
var replyValue = new TdResultMessage(response.result(), response.cause());
|
||||
try {
|
||||
msg.reply(replyValue, replyOpts);
|
||||
sink.next(response);
|
||||
} catch (Exception ex) {
|
||||
msg.fail(500, ex.getLocalizedMessage());
|
||||
sink.error(ex);
|
||||
}
|
||||
})
|
||||
.then()
|
||||
.doOnError(ex -> {
|
||||
logger.error("Error when processing a request", ex);
|
||||
})
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
private void undeploy(Runnable whenUndeployed) {
|
||||
botAlias.asMono().single().flatMap(botAlias -> {
|
||||
logger.info("Undeploy of bot \"" + botAlias + "\": stopping");
|
||||
return onBeforeStopListeners.asFlux()
|
||||
.collectList()
|
||||
.single()
|
||||
.flatMap(onBeforeStopListeners ->
|
||||
Mono.<Void>create(sink -> runAll(onBeforeStopListeners, MonoUtils.toHandler(sink)))
|
||||
.doOnError(ex -> logger.error("A beforeStop listener failed", ex)))
|
||||
.then(Flux
|
||||
.merge(unregisterConsumerOrLog(this.isWorkingConsumer.asMono(), "isWorkingConsumer"),
|
||||
unregisterConsumerOrLog(this.startConsumer.asMono(), "isWorkingConsumer"),
|
||||
unregisterConsumerOrLog(this.executeConsumer.asMono(), "isWorkingConsumer"))
|
||||
.then())
|
||||
.then(onAfterStopListeners.asFlux().collectList())
|
||||
.single()
|
||||
.doOnNext(onAfterStopListeners -> {
|
||||
logger.info("TdMiddle verticle \"" + botAddress + "\" stopped");
|
||||
|
||||
runAll(onAfterStopListeners, onAfterStopHandler -> {
|
||||
if (onAfterStopHandler.failed()) {
|
||||
logger.error("An afterStop listener failed: " + onAfterStopHandler.cause());
|
||||
}
|
||||
|
||||
logger.info("Undeploy of bot \"" + botAlias + "\": stopped");
|
||||
whenUndeployed.run();
|
||||
});
|
||||
});
|
||||
})
|
||||
.doOnError(ex -> logger.error("Error when stopping", ex))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private <T> Mono<Void> unregisterConsumerOrLog(Mono<MessageConsumer<T>> consumerMono, String consumerName) {
|
||||
return consumerMono
|
||||
.flatMap(consumer -> Mono
|
||||
.<Void>create(sink -> consumer.unregister(MonoUtils.toHandler(sink)))
|
||||
.onErrorResume(ex -> Mono.fromRunnable(() -> {
|
||||
logger.error("Can't unregister consumer \"" + consumerName + "\"", ex);
|
||||
})));
|
||||
}
|
||||
|
||||
private Mono<Void> pipe(AsyncTdDirectImpl td, String botAddress, String botAlias, boolean local) {
|
||||
var updatesFlux = td
|
||||
.receive(tdOptions)
|
||||
.doOnNext(update -> {
|
||||
if (OUTPUT_REQUESTS) {
|
||||
System.out.println(":=> " + msg
|
||||
.body()
|
||||
.getRequest()
|
||||
System.out.println("<=: " + update
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
.replace("\t", "")
|
||||
.replace(" ", "")
|
||||
.replace(" = ", "="));
|
||||
}
|
||||
td
|
||||
.execute(msg.body().getRequest(), msg.body().isExecuteDirectly())
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> {
|
||||
return TdResult.failed(new TdApi.Error(500, "Received null response"));
|
||||
}))
|
||||
.handle((response, sink) -> {
|
||||
try {
|
||||
msg.reply(new TdResultMessage(response.result(), response.cause()),
|
||||
cluster.newDeliveryOpts().setLocalOnly(local)
|
||||
);
|
||||
sink.next(response);
|
||||
} catch (Exception ex) {
|
||||
sink.error(ex);
|
||||
})
|
||||
.flatMap(item -> Mono.<TdResult<TdApi.Object>>create(sink -> {
|
||||
if (item.succeeded()) {
|
||||
var tdObject = item.result();
|
||||
if (tdObject instanceof Update) {
|
||||
var tdUpdate = (Update) tdObject;
|
||||
if (tdUpdate.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var tdUpdateAuthorizationState = (UpdateAuthorizationState) tdUpdate;
|
||||
if (tdUpdateAuthorizationState.authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.debug("Undeploying after receiving AuthorizationStateClosed");
|
||||
this.undeploy(() -> sink.success(item));
|
||||
return;
|
||||
}
|
||||
})
|
||||
.subscribe(response -> {}, ex -> {
|
||||
logger.error("Error when processing a request", ex);
|
||||
msg.fail(500, ex.getLocalizedMessage());
|
||||
});
|
||||
} catch (ClassCastException ex) {
|
||||
logger.error("Error when deserializing a request", ex);
|
||||
msg.fail(500, ex.getMessage());
|
||||
}
|
||||
});
|
||||
executeConsumer.completionHandler(MonoUtils.toHandler(registrationSink));
|
||||
});
|
||||
}
|
||||
|
||||
private void undeploy(Runnable whenUndeployed) {
|
||||
runAll(onBeforeStopListeners, onBeforeStopHandler -> {
|
||||
if (onBeforeStopHandler.failed()) {
|
||||
logger.error("A beforeStop listener failed: "+ onBeforeStopHandler.cause());
|
||||
}
|
||||
|
||||
Mono.create(sink -> this.isWorkingConsumer.unregister(result -> {
|
||||
if (result.failed()) {
|
||||
logger.error("Can't unregister consumer", result.cause());
|
||||
}
|
||||
this.startConsumer.unregister(result2 -> {
|
||||
if (result2.failed()) {
|
||||
logger.error("Can't unregister consumer", result2.cause());
|
||||
}
|
||||
|
||||
this.executeConsumer.unregister(result4 -> {
|
||||
if (result4.failed()) {
|
||||
logger.error("Can't unregister consumer", result4.cause());
|
||||
}
|
||||
} else if (tdObject instanceof Error) {
|
||||
// An error in updates means that a fatal error occurred
|
||||
logger.debug("Undeploying after receiving a fatal error");
|
||||
this.undeploy(() -> sink.success(item));
|
||||
return;
|
||||
}
|
||||
|
||||
sink.success();
|
||||
});
|
||||
});
|
||||
})).doFinally(signalType -> {
|
||||
logger.info("TdMiddle verticle \"" + botAddress + "\" stopped");
|
||||
|
||||
runAll(onAfterStopListeners, onAfterStopHandler -> {
|
||||
if (onAfterStopHandler.failed()) {
|
||||
logger.error("An afterStop listener failed: " + onAfterStopHandler.cause());
|
||||
sink.success(item);
|
||||
}
|
||||
|
||||
whenUndeployed.run();
|
||||
});
|
||||
}).subscribe(v -> {}, ex -> {
|
||||
logger.error("Error when stopping", ex);
|
||||
}, () -> {});
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> pipe() {
|
||||
var updatesFlux = td.receive(tdOptions).doOnNext(update -> {
|
||||
if (OUTPUT_REQUESTS) {
|
||||
System.out.println("<=: " + update
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
.replace("\t", "")
|
||||
.replace(" ", "")
|
||||
.replace(" = ", "="));
|
||||
}
|
||||
}).bufferTimeout(tdOptions.getEventsSize(), local ? Duration.ofMillis(1) : Duration.ofMillis(100))
|
||||
.filter(l -> !l.isEmpty())
|
||||
}))
|
||||
.bufferTimeout(tdOptions.getEventsSize(), local ? Duration.ofMillis(1) : Duration.ofMillis(100))
|
||||
.windowTimeout(1, Duration.ofSeconds(5))
|
||||
.flatMap(w -> w.defaultIfEmpty(Collections.emptyList()))
|
||||
.map(TdResultList::new).doFinally(s -> {
|
||||
.map(TdResultList::new).doOnTerminate(() -> {
|
||||
if (OUTPUT_REQUESTS) {
|
||||
System.out.println("<=: end (3)");
|
||||
}
|
||||
this.undeploy(() -> {});
|
||||
});
|
||||
var fluxCodec = new TdResultListMessageCodec();
|
||||
return EventBusFlux.<TdResultList>serve(updatesFlux,
|
||||
var tuple = EventBusFlux.<TdResultList>serve(updatesFlux,
|
||||
cluster.getEventBus(),
|
||||
botAddress + ".updates",
|
||||
cluster.newDeliveryOpts().setLocalOnly(local),
|
||||
fluxCodec,
|
||||
Duration.ofSeconds(30)
|
||||
);
|
||||
var served = tuple.getT1();
|
||||
var fatalError = tuple.getT2();
|
||||
//noinspection CallingSubscribeInNonBlockingScope
|
||||
fatalError
|
||||
.doOnNext(e -> logger.warn("Undeploying after a fatal error in a served flux"))
|
||||
.flatMap(error -> td.execute(new TdApi.Close(), false))
|
||||
.doOnError(ex -> logger.error("Unexpected error", ex))
|
||||
.subscribe();
|
||||
return served.doOnSubscribe(s -> {
|
||||
logger.debug("Preparing to serve bot \"" + botAlias + "\" updates flux...");
|
||||
}).doOnSuccess(v -> {
|
||||
logger.debug("Ready to serve bot \"" + botAlias + "\" updates flux");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.middle.direct.AsyncTdMiddleDirect;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
@ -7,7 +7,7 @@
|
||||
<http://logging.apache.org/log4j/2.x/manual/appenders.html>.
|
||||
-->
|
||||
|
||||
<Configuration status="INFO">
|
||||
<Configuration status="TRACE">
|
||||
<Appenders>
|
||||
|
||||
<!-- DEFAULT APPENDERS -->
|
||||
@ -27,7 +27,7 @@
|
||||
<Loggers>
|
||||
<Logger name="com.hazelcast.internal.diagnostics.HealthMonitor" level="WARN" />
|
||||
<Logger name="com.hazelcast" level="INFO" />
|
||||
<Root level="info">
|
||||
<Root level="TRACE">
|
||||
<filters>
|
||||
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
|
||||
onMismatch="NEUTRAL" />
|
||||
|
Loading…
Reference in New Issue
Block a user