338 lines
12 KiB
Java
338 lines
12 KiB
Java
package it.tdlight.tdlibsession.td.middle.client;
|
|
|
|
import io.vertx.circuitbreaker.CircuitBreaker;
|
|
import io.vertx.circuitbreaker.CircuitBreakerOptions;
|
|
import io.vertx.core.AbstractVerticle;
|
|
import io.vertx.core.AsyncResult;
|
|
import io.vertx.core.DeploymentOptions;
|
|
import io.vertx.core.Promise;
|
|
import io.vertx.core.eventbus.Message;
|
|
import io.vertx.core.json.JsonObject;
|
|
import it.tdlight.common.ConstructorDetector;
|
|
import it.tdlight.jni.TdApi;
|
|
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
|
import it.tdlight.jni.TdApi.Function;
|
|
import it.tdlight.jni.TdApi.Update;
|
|
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
|
import it.tdlight.tdlibsession.td.ResponseError;
|
|
import it.tdlight.tdlibsession.td.TdResult;
|
|
import it.tdlight.tdlibsession.td.TdResultMessage;
|
|
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
|
|
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
|
|
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
|
import it.tdlight.tdlibsession.td.middle.TdExecuteObjectMessageCodec;
|
|
import it.tdlight.tdlibsession.td.middle.TdMessageCodec;
|
|
import it.tdlight.tdlibsession.td.middle.TdOptListMessageCodec;
|
|
import it.tdlight.tdlibsession.td.middle.TdOptionalList;
|
|
import it.tdlight.tdlibsession.td.middle.TdResultMessageCodec;
|
|
import it.tdlight.utils.MonoUtils;
|
|
import java.time.Duration;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.StringJoiner;
|
|
import java.util.logging.Level;
|
|
import org.apache.commons.lang3.tuple.Pair;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.warp.commonutils.error.InitializationException;
|
|
import reactor.core.publisher.Flux;
|
|
import reactor.core.publisher.Mono;
|
|
import reactor.core.publisher.ReplayProcessor;
|
|
|
|
public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements AsyncTdMiddle {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusClient.class);
|
|
public static final boolean OUTPUT_REQUESTS = false;
|
|
public static final byte[] EMPTY = new byte[0];
|
|
|
|
private final ReplayProcessor<Boolean> tdClosed = ReplayProcessor.cacheLastOrDefault(false);
|
|
|
|
private ReplayProcessor<Flux<Update>> incomingUpdatesCo = ReplayProcessor.cacheLast();
|
|
|
|
private TdClusterManager cluster;
|
|
|
|
private String botAddress;
|
|
private String botAlias;
|
|
private boolean local;
|
|
private long initTime;
|
|
|
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
public AsyncTdMiddleEventBusClient(TdClusterManager clusterManager) {
|
|
cluster = clusterManager;
|
|
if (cluster.registerDefaultCodec(TdOptionalList.class, new TdOptListMessageCodec())) {
|
|
cluster.registerDefaultCodec(ExecuteObject.class, new TdExecuteObjectMessageCodec());
|
|
cluster.registerDefaultCodec(TdResultMessage.class, new TdResultMessageCodec());
|
|
for (Class<?> value : ConstructorDetector.getTDConstructorsUnsafe().values()) {
|
|
cluster.registerDefaultCodec(value, new TdMessageCodec(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
public static Mono<AsyncTdMiddleEventBusClient> getAndDeployInstance(TdClusterManager clusterManager, String botAlias, String botAddress, boolean local) throws InitializationException {
|
|
try {
|
|
var instance = new AsyncTdMiddleEventBusClient(clusterManager);
|
|
var options = new DeploymentOptions().setConfig(new JsonObject()
|
|
.put("botAddress", botAddress)
|
|
.put("botAlias", botAlias)
|
|
.put("local", local));
|
|
return MonoUtils.<String>executeAsFuture(promise -> {
|
|
clusterManager.getVertx().deployVerticle(instance, options, promise);
|
|
}).doOnNext(_v -> {
|
|
logger.trace("Deployed verticle for bot address: " + botAddress);
|
|
}).thenReturn(instance);
|
|
} catch (RuntimeException e) {
|
|
throw new InitializationException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void start(Promise<Void> startPromise) {
|
|
var botAddress = config().getString("botAddress");
|
|
if (botAddress == null || botAddress.isEmpty()) {
|
|
throw new IllegalArgumentException("botAddress is not set!");
|
|
}
|
|
this.botAddress = botAddress;
|
|
var botAlias = config().getString("botAlias");
|
|
if (botAlias == null || botAlias.isEmpty()) {
|
|
throw new IllegalArgumentException("botAlias is not set!");
|
|
}
|
|
this.botAlias = botAlias;
|
|
var local = config().getBoolean("local");
|
|
if (local == null) {
|
|
throw new IllegalArgumentException("local is not set!");
|
|
}
|
|
this.local = local;
|
|
this.initTime = System.currentTimeMillis();
|
|
|
|
CircuitBreaker startBreaker = CircuitBreaker.create("bot-" + botAddress + "-server-online-check-circuit-breaker", vertx,
|
|
new CircuitBreakerOptions().setMaxFailures(1).setMaxRetries(4).setTimeout(10000)
|
|
)
|
|
.retryPolicy(policy -> 4000L)
|
|
.openHandler(closed -> {
|
|
logger.error("Circuit opened! " + botAddress);
|
|
})
|
|
.closeHandler(closed -> {
|
|
logger.error("Circuit closed! " + botAddress);
|
|
});
|
|
|
|
startBreaker.execute(future -> {
|
|
try {
|
|
logger.error("Requesting " + botAddress + ".ping");
|
|
cluster
|
|
.getEventBus()
|
|
.request(botAddress + ".ping", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local), pingMsg -> {
|
|
if (pingMsg.succeeded()) {
|
|
logger.error("Received ping reply (succeeded)");
|
|
logger.error("Requesting " + botAddress + ".start");
|
|
cluster
|
|
.getEventBus()
|
|
.request(botAddress + ".start", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(10000), startMsg -> {
|
|
if (startMsg.succeeded()) {
|
|
logger.error("Requesting " + botAddress + ".isWorking");
|
|
cluster
|
|
.getEventBus()
|
|
.request(botAddress + ".isWorking", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(10000), msg -> {
|
|
if (msg.succeeded()) {
|
|
this.listen().then(this.pipe()).timeout(Duration.ofSeconds(10)).subscribe(v -> {}, future::fail, future::complete);
|
|
} else {
|
|
future.fail(msg.cause());
|
|
}
|
|
});
|
|
} else {
|
|
future.fail(startMsg.cause());
|
|
}
|
|
});
|
|
} else {
|
|
logger.error("Received ping reply (failed) (local=" + local + ")", pingMsg.cause());
|
|
future.fail(pingMsg.cause());
|
|
}
|
|
}
|
|
);
|
|
} catch (Exception ex) {
|
|
future.fail(ex);
|
|
}
|
|
})
|
|
.onFailure(ex -> {
|
|
logger.error("Failure when starting bot " + botAddress, ex);
|
|
startPromise.fail(new InitializationException("Can't connect tdlib middle client to tdlib middle server!"));
|
|
})
|
|
.onSuccess(v -> startPromise.complete());
|
|
}
|
|
|
|
@Override
|
|
public void stop(Promise<Void> stopPromise) {
|
|
tdClosed.onNext(true);
|
|
stopPromise.complete();
|
|
}
|
|
|
|
private Mono<Void> listen() {
|
|
// Nothing to listen for now
|
|
return Mono.empty();
|
|
}
|
|
|
|
private Mono<Void> pipe() {
|
|
incomingUpdatesCo.onNext(this.requestUpdatesBatchFromNetwork()
|
|
.repeatWhen(nFlux -> {
|
|
return Flux.push(emitter -> {
|
|
var dispos = Flux.combineLatest(nFlux, tdClosed, Pair::of).subscribe(val -> {
|
|
//noinspection PointlessBooleanExpression
|
|
if (val.getRight() == true) {
|
|
emitter.complete();
|
|
} else {
|
|
if (val.getLeft() == 0) {
|
|
emitter.complete();
|
|
} else {
|
|
emitter.next(val);
|
|
}
|
|
}
|
|
});
|
|
emitter.onDispose(dispos);
|
|
});
|
|
}) // Repeat when there is one batch with a flux of updates
|
|
.flatMap(batch -> batch)
|
|
.flatMap(update -> {
|
|
return Mono.<Update>create(sink -> {
|
|
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
|
var state = (UpdateAuthorizationState) update;
|
|
if (state.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
|
|
tdClosed.onNext(true);
|
|
this.getVertx().undeploy(this.deploymentID(), undeployed -> {
|
|
if (undeployed.failed()) {
|
|
logger.error("Error when undeploying td verticle", undeployed.cause());
|
|
}
|
|
sink.success(update);
|
|
});
|
|
} else {
|
|
sink.success(update);
|
|
}
|
|
} else {
|
|
sink.success(update);
|
|
}
|
|
});
|
|
})
|
|
.log("TdMiddle", Level.FINEST).publish().autoConnect(1));
|
|
return Mono.empty();
|
|
}
|
|
|
|
private static class UpdatesBatchResult {
|
|
public final Flux<Update> updatesFlux;
|
|
public final boolean completed;
|
|
|
|
private UpdatesBatchResult(Flux<Update> updatesFlux, boolean completed) {
|
|
this.updatesFlux = updatesFlux;
|
|
this.completed = completed;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return new StringJoiner(", ", UpdatesBatchResult.class.getSimpleName() + "[", "]")
|
|
.add("updatesFlux=" + updatesFlux)
|
|
.add("completed=" + completed)
|
|
.toString();
|
|
}
|
|
}
|
|
|
|
private Mono<Flux<TdApi.Update>> requestUpdatesBatchFromNetwork() {
|
|
return Mono
|
|
.from(tdClosed)
|
|
.single()
|
|
.filter(tdClosed -> !tdClosed)
|
|
.flatMap(_x -> Mono.<Flux<TdApi.Update>>create(sink -> {
|
|
cluster.getEventBus().<TdOptionalList>request(botAddress + ".getNextUpdatesBlock",
|
|
EMPTY,
|
|
cluster.newDeliveryOpts().setLocalOnly(local),
|
|
msg -> {
|
|
if (msg.failed()) {
|
|
//if (System.currentTimeMillis() - initTime <= 30000) {
|
|
// // The serve has not been started
|
|
// sink.success(Flux.empty());
|
|
//} else {
|
|
// // Timeout
|
|
sink.error(msg.cause());
|
|
//}
|
|
} else {
|
|
var result = msg.result();
|
|
if (result.body() == null) {
|
|
sink.success();
|
|
} else {
|
|
var resultBody = msg.result().body();
|
|
if (resultBody.isSet()) {
|
|
List<TdResult<Update>> updates = resultBody.getValues();
|
|
for (TdResult<Update> updateObj : updates) {
|
|
if (updateObj.succeeded()) {
|
|
if (OUTPUT_REQUESTS) {
|
|
System.out.println(" <- " + updateObj.result()
|
|
.toString()
|
|
.replace("\n", " ")
|
|
.replace("\t", "")
|
|
.replace(" ", "")
|
|
.replace(" = ", "="));
|
|
}
|
|
} else {
|
|
logger.error("Received an errored update",
|
|
ResponseError.newResponseError("incoming update", botAlias, updateObj.cause())
|
|
);
|
|
}
|
|
}
|
|
sink.success(Flux.fromIterable(updates).filter(TdResult::succeeded).map(TdResult::result));
|
|
} else {
|
|
// the stream has ended
|
|
sink.success();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}));
|
|
}
|
|
|
|
@Override
|
|
public Flux<Update> getUpdates() {
|
|
return incomingUpdatesCo.filter(Objects::nonNull).take(1).single().flatMapMany(v -> v);
|
|
}
|
|
|
|
@Override
|
|
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function request, boolean executeDirectly) {
|
|
|
|
var req = new ExecuteObject(executeDirectly, request);
|
|
if (OUTPUT_REQUESTS) {
|
|
System.out.println(" -> " + request.toString()
|
|
.replace("\n", " ")
|
|
.replace("\t", "")
|
|
.replace(" ", "")
|
|
.replace(" = ", "="));
|
|
}
|
|
|
|
return Mono.from(tdClosed).single()
|
|
.filter(tdClosed -> !tdClosed)
|
|
.flatMap(_x -> Mono.<TdResult<T>>create(sink -> {
|
|
cluster.getEventBus().request(botAddress + ".execute", req, cluster.newDeliveryOpts().setLocalOnly(local), (AsyncResult<Message<TdResultMessage>> event) -> {
|
|
if (event.succeeded()) {
|
|
if (event.result().body() == null) {
|
|
sink.success();
|
|
} else {
|
|
sink.success(Objects.requireNonNull(event.result().body()).toTdResult());
|
|
}
|
|
} else {
|
|
sink.error(ResponseError.newResponseError(request, botAlias, event.cause()));
|
|
}
|
|
});
|
|
|
|
})).flatMap(response -> {
|
|
try {
|
|
Objects.requireNonNull(response);
|
|
if (OUTPUT_REQUESTS) {
|
|
System.out.println(" <- " + response.toString()
|
|
.replace("\n", " ")
|
|
.replace("\t", "")
|
|
.replace(" ", "")
|
|
.replace(" = ", "="));
|
|
}
|
|
return Mono.just((TdResult<T>) response);
|
|
} catch (ClassCastException | NullPointerException e) {
|
|
return Mono.error(e);
|
|
}
|
|
});
|
|
}
|
|
}
|