config service: convert backing database to use mongodb

This is a much more natural fit for the config service which is really a
key value store, and has been abusing SQL. This will let us expand the
config stuff later to support profiles and per-account config values.

I have left in the SQL code for now so config changes are still being
sent there in the event of catastrophic mongodb failure.
This commit is contained in:
Adam
2019-03-20 18:37:50 -04:00
parent 327986fc56
commit 4ccefd23a4
4 changed files with 156 additions and 7 deletions

View File

@@ -116,6 +116,11 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>3.10.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -26,6 +26,8 @@ package net.runelite.http.service;
import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.LoggerContext;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
@@ -43,6 +45,7 @@ import okhttp3.OkHttpClient;
import org.slf4j.ILoggerFactory; import org.slf4j.ILoggerFactory;
import org.slf4j.impl.StaticLoggerBinder; import org.slf4j.impl.StaticLoggerBinder;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@@ -156,6 +159,12 @@ public class SpringBootWebApplication extends SpringBootServletInitializer
return createSql2oFromDataSource(dataSource); return createSql2oFromDataSource(dataSource);
} }
@Bean
public MongoClient mongoClient(@Value("${mongo.host}") String host)
{
return MongoClients.create(host);
}
private static DataSource getDataSource(DataSourceProperties dataSourceProperties) private static DataSource getDataSource(DataSourceProperties dataSourceProperties)
{ {
if (!Strings.isNullOrEmpty(dataSourceProperties.getJndiName())) if (!Strings.isNullOrEmpty(dataSourceProperties.getJndiName()))

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017, Adam <Adam@sigterm.info> * Copyright (c) 2017-2019, Adam <Adam@sigterm.info>
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@@ -24,10 +24,26 @@
*/ */
package net.runelite.http.service.config; package net.runelite.http.service.config;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import static com.mongodb.client.model.Filters.eq;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import static com.mongodb.client.model.Updates.set;
import static com.mongodb.client.model.Updates.unset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import net.runelite.http.api.RuneLiteAPI;
import net.runelite.http.api.config.ConfigEntry; import net.runelite.http.api.config.ConfigEntry;
import net.runelite.http.api.config.Configuration; import net.runelite.http.api.config.Configuration;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -36,6 +52,7 @@ import org.sql2o.Sql2o;
import org.sql2o.Sql2oException; import org.sql2o.Sql2oException;
@Service @Service
@Slf4j
public class ConfigService public class ConfigService
{ {
private static final String CREATE_CONFIG = "CREATE TABLE IF NOT EXISTS `config` (\n" private static final String CREATE_CONFIG = "CREATE TABLE IF NOT EXISTS `config` (\n"
@@ -49,10 +66,14 @@ public class ConfigService
+ " ADD CONSTRAINT `user_fk` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;"; + " ADD CONSTRAINT `user_fk` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;";
private final Sql2o sql2o; private final Sql2o sql2o;
private final Gson GSON = RuneLiteAPI.GSON;
private final MongoCollection<Document> mongoCollection;
@Autowired @Autowired
public ConfigService( public ConfigService(
@Qualifier("Runelite SQL2O") Sql2o sql2o @Qualifier("Runelite SQL2O") Sql2o sql2o,
MongoClient mongoClient
) )
{ {
this.sql2o = sql2o; this.sql2o = sql2o;
@@ -72,17 +93,61 @@ public class ConfigService
// Ignore, happens when index already exists // Ignore, happens when index already exists
} }
} }
MongoDatabase database = mongoClient.getDatabase("config");
MongoCollection<Document> collection = database.getCollection("config");
this.mongoCollection = collection;
// Create unique index on _userId
IndexOptions indexOptions = new IndexOptions().unique(true);
collection.createIndex(Indexes.ascending("_userId"), indexOptions);
}
private Document getConfig(int userId)
{
return mongoCollection.find(eq("_userId", userId)).first();
} }
public Configuration get(int userId) public Configuration get(int userId)
{ {
List<ConfigEntry> config; Map<String, Object> configMap = getConfig(userId);
try (Connection con = sql2o.open()) if (configMap == null || configMap.isEmpty())
{ {
config = con.createQuery("select `key`, value from config where user = :user") return null;
.addParameter("user", userId) }
.executeAndFetch(ConfigEntry.class);
List<ConfigEntry> config = new ArrayList<>();
for (String group : configMap.keySet())
{
// Reserved keys
if (group.startsWith("_") || group.startsWith("$"))
{
continue;
}
Map<String, Object> groupMap = (Map) configMap.get(group);
for (Map.Entry<String, Object> entry : groupMap.entrySet())
{
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Map || value instanceof Collection)
{
value = GSON.toJson(entry.getValue());
}
else if (value == null)
{
continue;
}
ConfigEntry configEntry = new ConfigEntry();
configEntry.setKey(group + "." + key.replace(':', '.'));
configEntry.setValue(value.toString());
config.add(configEntry);
}
} }
return new Configuration(config); return new Configuration(config);
@@ -102,6 +167,21 @@ public class ConfigService
.addParameter("value", value != null ? value : "") .addParameter("value", value != null ? value : "")
.executeUpdate(); .executeUpdate();
} }
if (key.startsWith("$") || key.startsWith("_"))
{
return;
}
String[] split = key.split("\\.", 2);
if (split.length != 2)
{
return;
}
Object jsonValue = parseJsonString(value);
mongoCollection.updateOne(eq("_userId", userId),
set(split[0] + "." + split[1].replace('.', ':'), jsonValue));
} }
public void unsetKey( public void unsetKey(
@@ -116,5 +196,57 @@ public class ConfigService
.addParameter("key", key) .addParameter("key", key)
.executeUpdate(); .executeUpdate();
} }
if (key.startsWith("$") || key.startsWith("_"))
{
return;
}
String[] split = key.split("\\.", 2);
if (split.length != 2)
{
return;
}
mongoCollection.updateOne(eq("_userId", userId),
unset(split[0] + "." + split[1].replace('.', ':')));
}
private static Object parseJsonString(String value)
{
Object jsonValue;
try
{
jsonValue = RuneLiteAPI.GSON.fromJson(value, Object.class);
if (jsonValue instanceof Double || jsonValue instanceof Float)
{
Number number = (Number) jsonValue;
if (Math.floor(number.doubleValue()) == number.doubleValue() && !Double.isInfinite(number.doubleValue()))
{
// value is an int or long. 'number' might be truncated so parse it from 'value'
try
{
jsonValue = Integer.parseInt(value);
}
catch (NumberFormatException ex)
{
try
{
jsonValue = Long.parseLong(value);
}
catch (NumberFormatException ex2)
{
}
}
}
}
}
catch (JsonSyntaxException ex)
{
jsonValue = value;
}
return jsonValue;
} }
} }

View File

@@ -29,6 +29,9 @@ redis:
pool.size: 10 pool.size: 10
host: http://localhost:6379 host: http://localhost:6379
mongo:
host: mongodb://localhost:27017
# Twitter client for feed # Twitter client for feed
runelite: runelite:
twitter: twitter: