2020-10-14 01:38:44 +02:00
|
|
|
package it.tdlight.tdlibsession.remoteclient;
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
import io.vertx.core.AsyncResult;
|
|
|
|
import io.vertx.core.Future;
|
2020-10-14 01:38:44 +02:00
|
|
|
import io.vertx.core.Handler;
|
2020-10-28 12:04:42 +01:00
|
|
|
import io.vertx.core.eventbus.Message;
|
2020-10-14 01:38:44 +02:00
|
|
|
import io.vertx.core.json.JsonObject;
|
|
|
|
import io.vertx.core.net.JksOptions;
|
2020-10-28 12:04:42 +01:00
|
|
|
import io.vertx.core.shareddata.AsyncMap;
|
2020-10-14 01:38:44 +02:00
|
|
|
import it.tdlight.common.Init;
|
|
|
|
import it.tdlight.common.utils.CantLoadLibrary;
|
|
|
|
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
|
|
|
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
import java.nio.file.FileAlreadyExistsException;
|
|
|
|
import java.nio.file.Files;
|
|
|
|
import java.nio.file.Path;
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
import java.util.Set;
|
|
|
|
import org.apache.logging.log4j.LogManager;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
2020-10-28 12:04:42 +01:00
|
|
|
import reactor.core.publisher.Flux;
|
|
|
|
import reactor.core.publisher.Sinks;
|
|
|
|
import reactor.core.publisher.Sinks.Many;
|
2021-01-13 17:22:14 +01:00
|
|
|
import reactor.core.scheduler.Scheduler;
|
|
|
|
import reactor.core.scheduler.Schedulers;
|
2020-10-14 01:38:44 +02:00
|
|
|
|
|
|
|
public class TDLibRemoteClient implements AutoCloseable {
|
|
|
|
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(TDLibRemoteClient.class);
|
|
|
|
|
|
|
|
private final SecurityInfo securityInfo;
|
|
|
|
private final String masterHostname;
|
|
|
|
private final String netInterface;
|
|
|
|
private final int port;
|
|
|
|
private final Set<String> membersAddresses;
|
2020-10-28 12:04:42 +01:00
|
|
|
private final Many<TdClusterManager> clusterManager = Sinks.many().replay().latest();
|
2021-01-13 17:22:14 +01:00
|
|
|
private final Scheduler vertxStatusScheduler = Schedulers.newSingle("VertxStatus", false);
|
2020-10-14 01:38:44 +02:00
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
public TDLibRemoteClient(SecurityInfo securityInfo, String masterHostname, String netInterface, int port, Set<String> membersAddresses) {
|
2020-10-14 01:38:44 +02:00
|
|
|
this.securityInfo = securityInfo;
|
|
|
|
this.masterHostname = masterHostname;
|
|
|
|
this.netInterface = netInterface;
|
|
|
|
this.port = port;
|
|
|
|
this.membersAddresses = membersAddresses;
|
|
|
|
|
|
|
|
try {
|
|
|
|
Init.start();
|
|
|
|
} catch (CantLoadLibrary ex) {
|
|
|
|
throw new RuntimeException(ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void main(String[] args) throws URISyntaxException {
|
|
|
|
if (args.length < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
String masterHostname = args[0];
|
|
|
|
|
|
|
|
String[] interfaceAndPort = args[1].split(":", 2);
|
|
|
|
|
|
|
|
String netInterface = interfaceAndPort[0];
|
|
|
|
|
|
|
|
int port = Integer.parseInt(interfaceAndPort[1]);
|
|
|
|
|
|
|
|
Set<String> membersAddresses = Set.of(args[2].split(","));
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
Path keyStorePath = Paths.get(args[3]);
|
|
|
|
Path keyStorePasswordPath = Paths.get(args[4]);
|
|
|
|
Path trustStorePath = Paths.get(args[5]);
|
|
|
|
Path trustStorePasswordPath = Paths.get(args[6]);
|
2020-10-14 01:38:44 +02:00
|
|
|
|
|
|
|
var loggerContext = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
|
|
|
|
loggerContext.setConfigLocation(TDLibRemoteClient.class.getResource("/tdlib-session-container-log4j2.xml").toURI());
|
|
|
|
|
|
|
|
var securityInfo = new SecurityInfo(keyStorePath, keyStorePasswordPath, trustStorePath, trustStorePasswordPath);
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
new TDLibRemoteClient(securityInfo, masterHostname, netInterface, port, membersAddresses).run(x -> {});
|
2020-10-14 01:38:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void start(Handler<Void> startedEventHandler) throws IllegalStateException {
|
|
|
|
run(startedEventHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void run(Handler<Void> startedEventHandler) {
|
|
|
|
try {
|
|
|
|
// Set verbosity level here, before creating the bots
|
|
|
|
if (Files.notExists(Paths.get("logs"))) {
|
|
|
|
try {
|
|
|
|
Files.createDirectory(Paths.get("logs"));
|
|
|
|
} catch (FileAlreadyExistsException ignored) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info("TDLib remote client is being hosted on" + netInterface + ":" + port + ". Master: " + masterHostname);
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
var botAddresses = new RemoteClientBotAddresses(Paths.get("remote_client_bot_addresses.txt"));
|
|
|
|
botAddresses.values().forEach(botAddress -> logger.info("Bot address is registered on this cluster:" + botAddress));
|
|
|
|
|
2020-10-14 01:38:44 +02:00
|
|
|
var keyStoreOptions = new JksOptions()
|
|
|
|
.setPath(securityInfo.getKeyStorePath().toAbsolutePath().toString())
|
|
|
|
.setPassword(securityInfo.getKeyStorePassword());
|
|
|
|
|
|
|
|
var trustStoreOptions = new JksOptions()
|
|
|
|
.setPath(securityInfo.getTrustStorePath().toAbsolutePath().toString())
|
|
|
|
.setPassword(securityInfo.getTrustStorePassword());
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
TdClusterManager.ofNodes(keyStoreOptions,
|
2020-10-14 01:38:44 +02:00
|
|
|
trustStoreOptions,
|
|
|
|
false,
|
|
|
|
masterHostname,
|
|
|
|
netInterface,
|
|
|
|
port,
|
|
|
|
membersAddresses
|
2020-10-28 12:04:42 +01:00
|
|
|
)
|
|
|
|
.doOnNext(clusterManager::tryEmitNext)
|
|
|
|
.doOnTerminate(clusterManager::tryEmitComplete)
|
|
|
|
.doOnError(clusterManager::tryEmitError)
|
|
|
|
.flatMapMany(clusterManager -> {
|
|
|
|
return Flux.create(sink -> {
|
2021-01-13 17:22:14 +01:00
|
|
|
clusterManager.getSharedData().getClusterWideMap("deployableBotAddresses", mapResult -> {
|
2020-10-28 12:04:42 +01:00
|
|
|
if (mapResult.succeeded()) {
|
|
|
|
var deployableBotAddresses = mapResult.result();
|
2020-10-14 01:38:44 +02:00
|
|
|
|
2021-01-13 17:22:14 +01:00
|
|
|
clusterManager.getSharedData().getLockWithTimeout("deployment", 15000, lockAcquisitionResult -> {
|
2020-10-28 12:04:42 +01:00
|
|
|
if (lockAcquisitionResult.succeeded()) {
|
|
|
|
var deploymentLock = lockAcquisitionResult.result();
|
|
|
|
putAllAsync(deployableBotAddresses, botAddresses.values(), (AsyncResult<Void> putAllResult) -> {
|
|
|
|
if (putAllResult.succeeded()) {
|
2020-10-28 12:27:10 +01:00
|
|
|
listenForDeployRequests(botAddresses,
|
|
|
|
clusterManager,
|
|
|
|
sink,
|
|
|
|
deployableBotAddresses
|
|
|
|
);
|
2020-10-28 12:04:42 +01:00
|
|
|
} else {
|
|
|
|
logger.error("Can't update shared map", putAllResult.cause());
|
|
|
|
sink.error(putAllResult.cause());
|
|
|
|
}
|
2020-10-28 12:27:10 +01:00
|
|
|
deploymentLock.release();
|
2020-10-28 12:04:42 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.error("Can't obtain deployment lock", lockAcquisitionResult.cause());
|
|
|
|
sink.error(lockAcquisitionResult.cause());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.error("Can't get shared map", mapResult.cause());
|
|
|
|
sink.error(mapResult.cause());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
2020-10-14 01:38:44 +02:00
|
|
|
.doOnError(ex -> {
|
|
|
|
logger.error(ex.getLocalizedMessage(), ex);
|
|
|
|
}).subscribe(i -> {}, e -> {}, () -> startedEventHandler.handle(null));
|
|
|
|
} catch (IOException ex) {
|
|
|
|
logger.error("Remote client error", ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-28 12:27:10 +01:00
|
|
|
private void listenForDeployRequests(RemoteClientBotAddresses botAddresses,
|
|
|
|
TdClusterManager clusterManager,
|
|
|
|
reactor.core.publisher.FluxSink<Object> sink,
|
|
|
|
AsyncMap<Object, Object> deployableBotAddresses) {
|
|
|
|
clusterManager.getEventBus().consumer("tdlib.remoteclient.clients.deploy", (Message<String> msg) -> {
|
|
|
|
|
|
|
|
clusterManager.getSharedData().getLockWithTimeout("deployment", 15000, lockAcquisitionResult -> {
|
|
|
|
if (lockAcquisitionResult.succeeded()) {
|
|
|
|
var deploymentLock = lockAcquisitionResult.result();
|
|
|
|
var botAddress = msg.body();
|
|
|
|
if (botAddresses.has(botAddress)) {
|
|
|
|
deployBot(clusterManager, botAddress, deploymentResult -> {
|
|
|
|
if (deploymentResult.failed()) {
|
|
|
|
msg.fail(500,
|
|
|
|
"Failed to deploy existing bot \"" + botAddress + "\": " + deploymentResult
|
|
|
|
.cause()
|
|
|
|
.getLocalizedMessage()
|
|
|
|
);
|
|
|
|
sink.error(deploymentResult.cause());
|
|
|
|
} else {
|
|
|
|
sink.next(botAddress);
|
|
|
|
}
|
|
|
|
deploymentLock.release();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
deployableBotAddresses.putIfAbsent(botAddress, netInterface, putResult -> {
|
|
|
|
if (putResult.succeeded()) {
|
|
|
|
if (putResult.result() == null) {
|
|
|
|
logger.info("Deploying new bot at address \"" + botAddress + "\"");
|
|
|
|
try {
|
|
|
|
botAddresses.putAddress(botAddress);
|
|
|
|
} catch (IOException e) {
|
|
|
|
logger.error("Can't save bot address \"" + botAddress + "\" to addresses file", e);
|
|
|
|
}
|
|
|
|
deployBot(clusterManager, botAddress, deploymentResult -> {
|
|
|
|
if (deploymentResult.failed()) {
|
|
|
|
msg.fail(500,
|
|
|
|
"Failed to deploy new bot \"" + botAddress + "\": " + deploymentResult
|
|
|
|
.cause()
|
|
|
|
.getLocalizedMessage()
|
|
|
|
);
|
|
|
|
sink.error(deploymentResult.cause());
|
|
|
|
} else {
|
|
|
|
sink.next(botAddress);
|
|
|
|
}
|
|
|
|
deploymentLock.release();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Ignore this bot because it's present on another cluster
|
|
|
|
deploymentLock.release();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.error("Can't update shared map", putResult.cause());
|
|
|
|
sink.error(putResult.cause());
|
|
|
|
deploymentLock.release();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.error("Can't obtain deployment lock", lockAcquisitionResult.cause());
|
|
|
|
sink.error(lockAcquisitionResult.cause());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-28 12:04:42 +01:00
|
|
|
private void deployBot(TdClusterManager clusterManager, String botAddress, Handler<AsyncResult<String>> deploymentHandler) {
|
|
|
|
AsyncTdMiddleEventBusServer verticle = new AsyncTdMiddleEventBusServer(clusterManager);
|
|
|
|
verticle.onBeforeStop(handler -> {
|
2021-01-13 17:22:14 +01:00
|
|
|
vertxStatusScheduler.schedule(() -> {
|
|
|
|
clusterManager.getSharedData().getLockWithTimeout("deployment", 15000, lockAcquisitionResult -> {
|
|
|
|
if (lockAcquisitionResult.succeeded()) {
|
|
|
|
var deploymentLock = lockAcquisitionResult.result();
|
|
|
|
verticle.onAfterStop(handler2 -> {
|
|
|
|
vertxStatusScheduler.schedule(() -> {
|
|
|
|
deploymentLock.release();
|
|
|
|
handler2.complete();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
clusterManager.getSharedData().getClusterWideMap("runningBotAddresses", (AsyncResult<AsyncMap<String, String>> mapResult) -> {
|
|
|
|
if (mapResult.succeeded()) {
|
|
|
|
var runningBotAddresses = mapResult.result();
|
|
|
|
runningBotAddresses.removeIfPresent(botAddress, netInterface, putResult -> {
|
|
|
|
if (putResult.succeeded()) {
|
|
|
|
if (putResult.result() != null) {
|
|
|
|
handler.complete();
|
|
|
|
} else {
|
|
|
|
handler.fail("Can't destroy bot with address \"" + botAddress + "\" because it has been already destroyed");
|
|
|
|
}
|
2020-10-28 12:04:42 +01:00
|
|
|
} else {
|
2021-01-13 17:22:14 +01:00
|
|
|
handler.fail(putResult.cause());
|
2020-10-28 12:04:42 +01:00
|
|
|
}
|
2021-01-13 17:22:14 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
handler.fail(mapResult.cause());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
handler.fail(lockAcquisitionResult.cause());
|
|
|
|
}
|
|
|
|
});
|
2020-10-28 12:04:42 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
clusterManager
|
|
|
|
.getVertx()
|
|
|
|
.deployVerticle(verticle,
|
|
|
|
clusterManager
|
|
|
|
.newDeploymentOpts()
|
|
|
|
.setConfig(new JsonObject()
|
|
|
|
.put("botAddress", botAddress)
|
|
|
|
.put("botAlias", botAddress)
|
|
|
|
.put("local", false)),
|
|
|
|
(deployed) -> {
|
|
|
|
if (deployed.failed()) {
|
|
|
|
logger.error("Can't deploy bot \"" + botAddress + "\"", deployed.cause());
|
|
|
|
}
|
|
|
|
deploymentHandler.handle(deployed);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void putAllAsync(AsyncMap<Object, Object> sharedMap,
|
|
|
|
Set<String> valuesToAdd,
|
|
|
|
Handler<AsyncResult<Void>> resultHandler) {
|
|
|
|
if (valuesToAdd.isEmpty()) {
|
|
|
|
resultHandler.handle(Future.succeededFuture());
|
|
|
|
} else {
|
|
|
|
var valueToAdd = valuesToAdd.stream().findFirst().get();
|
|
|
|
valuesToAdd.remove(valueToAdd);
|
|
|
|
sharedMap.putIfAbsent(valueToAdd, netInterface, result -> {
|
|
|
|
if (result.succeeded()) {
|
|
|
|
if (result.result() == null || result.result().equals(netInterface)) {
|
|
|
|
putAllAsync(sharedMap, valuesToAdd, resultHandler);
|
|
|
|
} else {
|
|
|
|
resultHandler.handle(Future.failedFuture(new UnsupportedOperationException("Key already present! Key: \"" + valueToAdd + "\", Value: \"" + result.result() + "\"")));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
resultHandler.handle(Future.failedFuture(result.cause()));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-14 01:38:44 +02:00
|
|
|
@Override
|
|
|
|
public void close() {
|
2020-10-28 12:04:42 +01:00
|
|
|
clusterManager.asFlux().blockFirst();
|
2021-01-13 17:22:14 +01:00
|
|
|
vertxStatusScheduler.dispose();
|
2020-10-14 01:38:44 +02:00
|
|
|
}
|
|
|
|
}
|