Skip to content

Commit 6b963bb

Browse files
committed
database refactor
1 parent c59d5e0 commit 6b963bb

18 files changed

+409
-239
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ target/
2828
log/
2929
.env
3030

31-
guild-config.db
32-
guild-config.db-journal
31+
*.db
32+
*.db-journal
3333
backups/

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@
143143
<artifactId>gson</artifactId>
144144
<version>2.13.1</version>
145145
</dependency>
146+
<dependency>
147+
<groupId>org.jdbi</groupId>
148+
<artifactId>jdbi3-sqlite</artifactId>
149+
<version>3.49.5</version>
150+
</dependency>
146151
<dependency>
147152
<groupId>org.junit.jupiter</groupId>
148153
<artifactId>junit-jupiter</artifactId>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package vc.config;
2+
3+
import org.jdbi.v3.core.Jdbi;
4+
import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
5+
import org.jdbi.v3.sqlite3.SQLitePlugin;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.DisposableBean;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.stereotype.Component;
11+
import vc.config.live_feed.LiveFeedConfigRecord;
12+
import vc.config.migrations.DatabaseMigrator;
13+
14+
import java.io.File;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
import java.sql.Connection;
18+
import java.sql.DriverManager;
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.time.ZoneId;
22+
import java.time.format.DateTimeFormatter;
23+
import java.util.Locale;
24+
import java.util.Optional;
25+
26+
@Component
27+
public class ConfigDatabase implements DisposableBean {
28+
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigDatabase.class);
29+
// backups older than this date will be deleted
30+
private static final Duration ROLLING_BACKUP_DURATION = Duration.ofDays(7);
31+
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss").withLocale(
32+
Locale.US).withZone(
33+
ZoneId.of("America/Los_Angeles"));
34+
private final Path backupPath = Paths.get("backups");
35+
private final Connection connection;
36+
private final RemoteDatabaseBackup remoteDatabaseBackup;
37+
// special mode where we sync from remote and don't upload backups.
38+
// intended for syncing to remote state on a new server or a local dev machine
39+
private final boolean dbSync;
40+
private final Jdbi jdbi;
41+
42+
public ConfigDatabase(
43+
DatabaseMigrator migrator,
44+
final RemoteDatabaseBackup remoteDatabaseBackup,
45+
@Value("${DB_SYNC}") final String dbSync
46+
) {
47+
this.dbSync = Boolean.parseBoolean(dbSync);
48+
this.remoteDatabaseBackup = remoteDatabaseBackup;
49+
if (this.dbSync) this.remoteDatabaseBackup.syncFromRemote();
50+
final Path dbPath = Paths.get("config.db");
51+
try {
52+
connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
53+
} catch (final Exception e) {
54+
LOGGER.error("Error initializing config database connection", e);
55+
throw new RuntimeException(e);
56+
}
57+
if (!this.dbSync) backupDatabase();
58+
jdbi = Jdbi.create(connection);
59+
new SQLitePlugin().customizeJdbi(jdbi);
60+
jdbi.registerRowMapper(ConstructorMapper.factory(LiveFeedConfigRecord.class));
61+
migrator.migrate(dbPath, jdbi);
62+
}
63+
64+
@Override
65+
public void destroy() {
66+
LOGGER.info("Shutting down ConfigDatabase");
67+
try {
68+
backupDatabase();
69+
connection.close();
70+
} catch (final Exception e) {
71+
LOGGER.error("Error closing config database connection", e);
72+
}
73+
}
74+
75+
public Jdbi getJdbi() {
76+
return jdbi;
77+
}
78+
79+
public void backupDatabase() {
80+
try {
81+
if (!backupPath.toFile().exists()) {
82+
backupPath.toFile().mkdirs();
83+
}
84+
var backupPath = "backups/config-backup-" + DATE_FORMATTER.format(Instant.now()) + ".db";
85+
try (var statement = connection.createStatement()) {
86+
statement.executeUpdate("BACKUP TO '" + backupPath + "'");
87+
}
88+
if (!dbSync) remoteDatabaseBackup.uploadDatabaseBackup(backupPath);
89+
} catch (final Exception e) {
90+
LOGGER.error("Error backing up config database", e);
91+
}
92+
cleanOldBackups();
93+
}
94+
95+
private void cleanOldBackups() {
96+
try {
97+
if (!backupPath.toFile().exists()) {
98+
return;
99+
}
100+
File[] files = backupPath.toFile().listFiles();
101+
if (files == null) {
102+
LOGGER.warn("no backups found?");
103+
return;
104+
}
105+
for (final File file : files) {
106+
if (file.getName().startsWith("config-backup-")) {
107+
final String dateString = file.getName().substring("config-backup-".length(), "config-backup-".length() + "yyyy-MM-dd-HH-mm-ss".length());
108+
final Instant date = Instant.from(DATE_FORMATTER.parse(dateString));
109+
if (date.isBefore(Instant.now().minus(ROLLING_BACKUP_DURATION))) {
110+
LOGGER.info("Deleting old config database backup {}", file.getName());
111+
if (!file.delete()) {
112+
LOGGER.warn("Failed to delete old config database backup {}", file.getName());
113+
}
114+
}
115+
}
116+
}
117+
LOGGER.info("Completed cleaning old backups");
118+
} catch (final Exception e) {
119+
LOGGER.error("Error cleaning old config database backups", e);
120+
}
121+
}
122+
123+
public Optional<LiveFeedConfigRecord> getLiveFeedConfigRecord(final String guildId) {
124+
try (var handle = jdbi.open()) {
125+
return handle.select("SELECT * FROM live_feed_config WHERE guild_id = :guildId")
126+
.bind("guildId", guildId)
127+
.mapTo(LiveFeedConfigRecord.class)
128+
.findFirst();
129+
} catch (final Exception e) {
130+
LOGGER.error("Error retrieving live feed config record for guild {}", guildId, e);
131+
return Optional.empty();
132+
}
133+
}
134+
135+
public void writeGuildConfigRecord(final LiveFeedConfigRecord config) {
136+
try (var handle = jdbi.open()) {
137+
handle.createUpdate("""
138+
INSERT OR REPLACE INTO live_feed_config VALUES (
139+
:guildId,
140+
:guildName,
141+
:liveChatEnabled,
142+
:liveChatChannelId,
143+
:liveConnectionsEnabled,
144+
:liveConnectionsChannelId)
145+
""")
146+
.bind("guildId", config.guildId())
147+
.bind("guildName", config.guildName())
148+
.bind("liveChatEnabled", config.liveChatEnabled())
149+
.bind("liveChatChannelId", config.liveChatChannelId())
150+
.bind("liveConnectionsEnabled", config.liveConnectionsEnabled())
151+
.bind("liveConnectionsChannelId", config.liveConnectionsChannelId())
152+
.execute();
153+
}
154+
}
155+
}

src/main/java/vc/config/GuildConfigDatabase.java

Lines changed: 0 additions & 161 deletions
This file was deleted.

src/main/java/vc/config/GuildConfigRecord.java

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/main/java/vc/config/RemoteDatabaseBackup.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public void syncFromRemote() {
5353
downloadDatabaseBackup(path);
5454
} catch (final Exception e) {
5555
LOGGER.error("Error syncing database from remote", e);
56-
throw new RuntimeException(e);
5756
}
5857
}
5958

@@ -63,7 +62,7 @@ public String findLatestDatabaseBackup() {
6362
minioClient.listObjects(
6463
ListObjectsArgs.builder()
6564
.bucket(bucketName)
66-
.prefix("backups/guild-config-backup-")
65+
.prefix("backups/config-backup-")
6766
.build())
6867
.spliterator(), false)
6968
.map(RemoteDatabaseBackup::retrieveS3ItemData)
@@ -90,7 +89,7 @@ private static Item retrieveS3ItemData(Result<Item> r) {
9089

9190
public void downloadDatabaseBackup(final String backupPath) {
9291
try {
93-
var out = Paths.get("guild-config.db");
92+
var out = Paths.get("config.db");
9493
Files.deleteIfExists(out);
9594
var data = minioClient.getObject(
9695
GetObjectArgs.builder()

0 commit comments

Comments
 (0)