Skip to content

Commit

Permalink
Switch most instance data to database
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexProgrammerDE committed Dec 29, 2024
1 parent b6d7e7a commit cd94701
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 81 deletions.
75 changes: 52 additions & 23 deletions server/src/main/java/com/soulfiremc/server/InstanceManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@
import com.soulfiremc.server.settings.lib.SettingsDelegate;
import com.soulfiremc.server.util.MathHelper;
import com.soulfiremc.server.util.TimeUtil;
import com.soulfiremc.server.util.structs.CachedLazyObject;
import com.soulfiremc.server.viaversion.SFVersionConstants;
import io.netty.channel.EventLoopGroup;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -64,21 +66,22 @@ public class InstanceManager {
private final SoulFireScheduler scheduler;
private final SettingsDelegate settingsSource;
private final SoulFireServer soulFireServer;
private final InstanceEntity instanceEntity;
private final SessionFactory sessionFactory;

public InstanceManager(SoulFireServer soulFireServer, InstanceEntity instanceEntity) {
public InstanceManager(SoulFireServer soulFireServer, SessionFactory sessionFactory, InstanceEntity instanceEntity) {
this.id = instanceEntity.id();
this.logger = LoggerFactory.getLogger("InstanceManager-" + id);
this.scheduler = new SoulFireScheduler(logger);
this.soulFireServer = soulFireServer;
this.settingsSource = new SettingsDelegate(instanceEntity);
this.instanceEntity = instanceEntity;
this.sessionFactory = sessionFactory;
this.settingsSource = new SettingsDelegate(new CachedLazyObject<>(() ->
sessionFactory.fromTransaction(session -> session.find(InstanceEntity.class, id).settings()), 1, TimeUnit.SECONDS));

this.scheduler.scheduleWithFixedDelay(this::tick, 0, 500, TimeUnit.MILLISECONDS);
this.scheduler.scheduleWithFixedDelay(this::refreshExpiredAccounts, 0, 1, TimeUnit.HOURS);

if (settingsSource.get(BotSettings.RESTORE_ON_REBOOT)) {
switchToState(instanceEntity.attackLifecycle());
switchToState(attackLifecycle());
}
}

Expand All @@ -100,7 +103,7 @@ private static Optional<SFProxy> getProxy(List<ProxyData> proxies) {
}

private void tick() {
if (instanceEntity.attackLifecycle().isTicking()) {
if (attackLifecycle().isTicking()) {
SoulFireAPI.postEvent(new AttackTickEvent(this));
}
}
Expand Down Expand Up @@ -129,7 +132,13 @@ private void refreshExpiredAccounts() {

if (refreshed > 0) {
logger.info("Refreshed {} accounts", refreshed);
instanceEntity.settings(instanceEntity.settings().withAccounts(accounts));
sessionFactory.inTransaction(session -> {
var instanceEntity = session.find(InstanceEntity.class, id);

instanceEntity.settings(instanceEntity.settings().withAccounts(accounts));

session.merge(instanceEntity);
});
}
}

Expand All @@ -143,41 +152,47 @@ private MinecraftAccount refreshAccount(MinecraftAccount account) {
var refreshedAccount = authService.refresh(account, null).join();
var accounts = new ArrayList<>(settingsSource.accounts());
accounts.set(accounts.indexOf(account), refreshedAccount);
instanceEntity.settings(instanceEntity.settings().withAccounts(accounts));
sessionFactory.inTransaction(session -> {
var instanceEntity = session.find(InstanceEntity.class, id);

instanceEntity.settings(instanceEntity.settings().withAccounts(accounts));

session.merge(instanceEntity);
});

return refreshedAccount;
}

public CompletableFuture<?> switchToState(AttackLifecycle targetState) {
return switch (targetState) {
case STARTING, RUNNING -> switch (instanceEntity.attackLifecycle()) {
case STARTING, RUNNING -> switch (attackLifecycle()) {
case STARTING, RUNNING, STOPPING -> CompletableFuture.completedFuture(null);
case PAUSED -> scheduler.runAsync(() -> this.instanceEntity.attackLifecycle(AttackLifecycle.RUNNING));
case PAUSED -> scheduler.runAsync(() -> this.attackLifecycle(AttackLifecycle.RUNNING));
case STOPPED -> scheduler.runAsync(this::start);
};
case PAUSED -> switch (instanceEntity.attackLifecycle()) {
case STARTING, RUNNING -> scheduler.runAsync(() -> this.instanceEntity.attackLifecycle(AttackLifecycle.PAUSED));
case PAUSED -> switch (attackLifecycle()) {
case STARTING, RUNNING -> scheduler.runAsync(() -> this.attackLifecycle(AttackLifecycle.PAUSED));
case STOPPING, PAUSED -> CompletableFuture.completedFuture(null);
case STOPPED -> scheduler.runAsync(() -> {
start();
this.instanceEntity.attackLifecycle(AttackLifecycle.PAUSED);
this.attackLifecycle(AttackLifecycle.PAUSED);
});
};
case STOPPING, STOPPED -> switch (instanceEntity.attackLifecycle()) {
case STOPPING, STOPPED -> switch (attackLifecycle()) {
case STARTING, RUNNING, PAUSED -> stopAttackPermanently();
case STOPPING, STOPPED -> CompletableFuture.completedFuture(null);
};
};
}

private void start() {
if (!instanceEntity.attackLifecycle().isFullyStopped()) {
if (!attackLifecycle().isFullyStopped()) {
throw new IllegalStateException("Another attack is still running");
}

SoulFireServer.setupLoggingAndVia(settingsSource);

this.instanceEntity.attackLifecycle(AttackLifecycle.STARTING);
this.attackLifecycle(AttackLifecycle.STARTING);

var address = settingsSource.get(BotSettings.ADDRESS);
logger.info("Preparing bot attack at {}", address);
Expand Down Expand Up @@ -292,11 +307,11 @@ private void start() {
logger.debug("Scheduling bot {}", factory.minecraftAccount().lastKnownName());
scheduler.schedule(
() -> {
if (instanceEntity.attackLifecycle().isStoppedOrStopping()) {
if (attackLifecycle().isStoppedOrStopping()) {
return;
}

TimeUtil.waitCondition(() -> instanceEntity.attackLifecycle().isPaused());
TimeUtil.waitCondition(() -> attackLifecycle().isPaused());

logger.debug("Connecting bot {}", factory.minecraftAccount().lastKnownName());
var botConnection = factory.prepareConnection();
Expand All @@ -316,8 +331,8 @@ private void start() {
TimeUnit.MILLISECONDS);
}

if (this.instanceEntity.attackLifecycle() == AttackLifecycle.STARTING) {
this.instanceEntity.attackLifecycle(AttackLifecycle.RUNNING);
if (this.attackLifecycle() == AttackLifecycle.STARTING) {
this.attackLifecycle(AttackLifecycle.RUNNING);
}
});
}
Expand All @@ -331,20 +346,34 @@ public CompletableFuture<?> shutdownHook() {
}

public CompletableFuture<?> stopAttackPermanently() {
if (instanceEntity.attackLifecycle().isStoppedOrStopping()) {
if (attackLifecycle().isStoppedOrStopping()) {
return CompletableFuture.completedFuture(null);
}

logger.info("Stopping bot attack");
this.instanceEntity.attackLifecycle(AttackLifecycle.STOPPING);
this.attackLifecycle(AttackLifecycle.STOPPING);

return this.stopAttackSession()
.thenRun(() -> {
this.instanceEntity.attackLifecycle(AttackLifecycle.STOPPED);
this.attackLifecycle(AttackLifecycle.STOPPED);
logger.info("Attack stopped");
});
}

private void attackLifecycle(AttackLifecycle attackLifecycle) {
sessionFactory.inTransaction(session -> {
var instanceEntity = session.find(InstanceEntity.class, id);

instanceEntity.attackLifecycle(attackLifecycle);

session.merge(instanceEntity);
});
}

private AttackLifecycle attackLifecycle() {
return sessionFactory.fromTransaction(session -> session.find(InstanceEntity.class, id).attackLifecycle());
}

public CompletableFuture<?> stopAttackSession() {
return scheduler.runAsync(() -> {
logger.info("Disconnecting bots");
Expand Down
30 changes: 8 additions & 22 deletions server/src/main/java/com/soulfiremc/server/SoulFireServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
* The main class of the SoulFire server.
Expand Down Expand Up @@ -116,6 +115,8 @@ public SoulFireServer(
injector.register(ShutdownManager.class, shutdownManager);

this.sessionFactory = DatabaseManager.forSqlite(baseDirectory.resolve("soulfire.sqlite"));
injector.register(SessionFactory.class, sessionFactory);

this.authSystem = new AuthSystem(this);
this.rpcServer = new RPCServer(host, port, injector, authSystem);

Expand Down Expand Up @@ -185,9 +186,6 @@ public SoulFireServer(
log.info("Loading instances...");
loadInstances();

log.info("Starting scheduled tasks...");
scheduler.scheduleWithFixedDelay(this::saveInstances, 0, 1, TimeUnit.SECONDS);

var rpcServerStart =
CompletableFuture.runAsync(
() -> {
Expand Down Expand Up @@ -221,7 +219,7 @@ private void loadInstances() {
for (var instanceData : sessionFactory.fromTransaction(s ->
s.createQuery("FROM InstanceEntity", InstanceEntity.class).getResultList())) {
try {
var instance = new InstanceManager(this, instanceData);
var instance = new InstanceManager(this, sessionFactory, instanceData);
SoulFireAPI.postEvent(new InstanceInitEvent(instance));

instances.put(instance.id(), instance);
Expand All @@ -236,22 +234,6 @@ private void loadInstances() {
}
}

private void saveInstances() {
try {
sessionFactory.inTransaction(s -> {
instances.values().stream()
.map(InstanceManager::instanceEntity)
.forEach(s::merge);

s.createMutationQuery("DELETE FROM InstanceEntity WHERE id NOT IN (:ids)")
.setParameterList("ids", instances.keySet())
.executeUpdate();
});
} catch (Exception e) {
log.error("Failed to save instances", e);
}
}

private void shutdownHook() {
// Shut down RPC
try {
Expand Down Expand Up @@ -279,7 +261,7 @@ public UUID createInstance(String friendlyName, SoulFireUser owner) {

return newInstanceEntity;
});
var instanceManager = new InstanceManager(this, instanceEntity);
var instanceManager = new InstanceManager(this, sessionFactory, instanceEntity);
SoulFireAPI.postEvent(new InstanceInitEvent(instanceManager));

instances.put(instanceManager.id(), instanceManager);
Expand All @@ -296,6 +278,10 @@ public CompletableFuture<?> shutdownInstances() {
}

public Optional<CompletableFuture<?>> deleteInstance(UUID id) {
sessionFactory.inTransaction(s -> s.createMutationQuery("DELETE FROM InstanceEntity WHERE id = :id")
.setParameter("id", id)
.executeUpdate());

return Optional.ofNullable(instances.remove(id)).map(InstanceManager::deleteInstance);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static SessionFactory forSqlite(Path dbFile) {
configuration.setProperty("hibernate.connection.driver_class", "org.sqlite.JDBC");
configuration.setProperty("hibernate.connection.url", "jdbc:sqlite:" + dbFile.toString());
configuration.setProperty("hibernate.connection.pool_size", 1);
configuration.setProperty("hibernate.show_sql", true);
// configuration.setProperty("hibernate.show_sql", true);
configuration.setProperty("hibernate.hbm2ddl.auto", "update");
configuration.setProperty("hibernate.hikari.minimumIdle", 1);
configuration.setProperty("hibernate.hikari.maximumPoolSize", 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import com.soulfiremc.grpc.generated.*;
import com.soulfiremc.server.SoulFireServer;
import com.soulfiremc.server.api.AttackLifecycle;
import com.soulfiremc.server.database.InstanceEntity;
import com.soulfiremc.server.settings.lib.SettingsImpl;
import com.soulfiremc.server.user.PermissionContext;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;

import javax.inject.Inject;
import java.util.Arrays;
Expand All @@ -37,6 +39,7 @@
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class InstanceServiceImpl extends InstanceServiceGrpc.InstanceServiceImplBase {
private final SoulFireServer soulFireServer;
private final SessionFactory sessionFactory;

private Collection<InstancePermissionState> getInstancePermissions(UUID instanceId) {
var user = ServerRPCConstants.USER_CONTEXT_KEY.get();
Expand Down Expand Up @@ -87,12 +90,12 @@ public void deleteInstance(InstanceDeleteRequest request, StreamObserver<Instanc
public void listInstances(InstanceListRequest request, StreamObserver<InstanceListResponse> responseObserver) {
try {
responseObserver.onNext(InstanceListResponse.newBuilder()
.addAllInstances(soulFireServer.instances().values().stream()
.addAllInstances(sessionFactory.fromTransaction(session -> session.createQuery("FROM InstanceEntity", InstanceEntity.class).list()).stream()
.filter(instance -> ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermission(PermissionContext.instance(InstancePermission.READ_INSTANCE, instance.id())))
.map(instance -> InstanceListResponse.Instance.newBuilder()
.setId(instance.id().toString())
.setFriendlyName(instance.instanceEntity().friendlyName())
.setState(instance.instanceEntity().attackLifecycle().toProto())
.setFriendlyName(instance.friendlyName())
.setState(instance.attackLifecycle().toProto())
.addAllInstancePermissions(getInstancePermissions(instance.id()))
.build())
.toList())
Expand All @@ -110,16 +113,15 @@ public void getInstanceInfo(InstanceInfoRequest request, StreamObserver<Instance
ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.instance(InstancePermission.READ_INSTANCE, instanceId));

try {
var optionalInstance = soulFireServer.getInstance(instanceId);
if (optionalInstance.isEmpty()) {
var instanceEntity = sessionFactory.fromTransaction(session -> session.find(InstanceEntity.class, instanceId));
if (instanceEntity == null) {
throw new StatusRuntimeException(Status.NOT_FOUND.withDescription("Instance '%s' not found".formatted(instanceId)));
}

var instance = optionalInstance.get();
responseObserver.onNext(InstanceInfoResponse.newBuilder()
.setFriendlyName(instance.instanceEntity().friendlyName())
.setConfig(instance.instanceEntity().settings().toProto())
.setState(instance.instanceEntity().attackLifecycle().toProto())
.setFriendlyName(instanceEntity.friendlyName())
.setConfig(instanceEntity.settings().toProto())
.setState(instanceEntity.attackLifecycle().toProto())
.addAllInstancePermissions(getInstancePermissions(instanceId))
.build());
responseObserver.onCompleted();
Expand All @@ -135,13 +137,16 @@ public void updateInstanceFriendlyName(InstanceUpdateFriendlyNameRequest request
ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.instance(InstancePermission.UPDATE_INSTANCE, instanceId));

try {
var optionalInstance = soulFireServer.getInstance(instanceId);
if (optionalInstance.isEmpty()) {
throw new StatusRuntimeException(Status.NOT_FOUND.withDescription("Instance '%s' not found".formatted(instanceId)));
}
sessionFactory.inTransaction(session -> {
var instanceEntity = session.find(InstanceEntity.class, instanceId);
if (instanceEntity == null) {
throw new StatusRuntimeException(Status.NOT_FOUND.withDescription("Instance '%s' not found".formatted(instanceId)));
}

var instance = optionalInstance.get();
instance.instanceEntity().friendlyName(request.getFriendlyName());
instanceEntity.friendlyName(request.getFriendlyName());

session.merge(instanceEntity);
});

responseObserver.onNext(InstanceUpdateFriendlyNameResponse.newBuilder().build());
responseObserver.onCompleted();
Expand All @@ -157,13 +162,16 @@ public void updateInstanceConfig(InstanceUpdateConfigRequest request, StreamObse
ServerRPCConstants.USER_CONTEXT_KEY.get().hasPermissionOrThrow(PermissionContext.instance(InstancePermission.UPDATE_INSTANCE, instanceId));

try {
var optionalInstance = soulFireServer.getInstance(instanceId);
if (optionalInstance.isEmpty()) {
throw new StatusRuntimeException(Status.NOT_FOUND.withDescription("Instance '%s' not found".formatted(instanceId)));
}
sessionFactory.inTransaction(session -> {
var instanceEntity = session.find(InstanceEntity.class, instanceId);
if (instanceEntity == null) {
throw new StatusRuntimeException(Status.NOT_FOUND.withDescription("Instance '%s' not found".formatted(instanceId)));
}

var instance = optionalInstance.get();
instance.instanceEntity().settings(SettingsImpl.fromProto(request.getConfig()));
instanceEntity.settings(SettingsImpl.fromProto(request.getConfig()));

session.merge(instanceEntity);
});

responseObserver.onNext(InstanceUpdateConfigResponse.newBuilder().build());
responseObserver.onCompleted();
Expand Down
Loading

0 comments on commit cd94701

Please sign in to comment.