Remove per-guild schema

All guild data will now be stored within the same table, instead of
a new schema being created for each guild. This avoids issues with
missing schemas.
This likely wasn't a good idea to begin with.
This commit is contained in:
Noikoio 2018-12-18 16:21:35 -08:00
parent de8660d913
commit 47a738ddbc
5 changed files with 111 additions and 139 deletions

View file

@ -69,12 +69,9 @@ namespace Kerobot
/// <param name="guild">
/// If manipulating guild-specific information, this parameter sets the database connection's search path.
/// </param>
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync(ulong? guild)
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync()
{
string cs = Config.PostgresConnString;
if (guild.HasValue) cs += ";searchpath=guild_" + guild.Value;
var db = new NpgsqlConnection(cs);
var db = new NpgsqlConnection(Config.PostgresConnString);
await db.OpenAsync();
return db;
}

View file

@ -59,7 +59,8 @@ namespace Kerobot
var client = new DiscordSocketClient(new DiscordSocketConfig()
{
DefaultRetryMode = RetryMode.AlwaysRetry,
MessageCacheSize = 0 // using our own
MessageCacheSize = 0, // using our own
LogLevel = LogSeverity.Info
});
// Kerobot class initialization - will set up services and modules

View file

@ -1,10 +1,9 @@
using Discord.WebSocket;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Npgsql;
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord.WebSocket;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Kerobot.Services.GuildState
{
@ -14,8 +13,6 @@ namespace Kerobot.Services.GuildState
/// </summary>
class GuildStateService : Service
{
// SCHEMAS ARE CREATED HERE, SOMEWHERE BELOW. MAKE SURE THIS CONSTRUCTOR IS CALLED EARLY.
private readonly object _storageLock = new object();
private readonly Dictionary<ulong, Dictionary<Type, StateInfo>> _storage;
@ -24,8 +21,7 @@ namespace Kerobot.Services.GuildState
public GuildStateService(Kerobot kb) : base(kb)
{
_storage = new Dictionary<ulong, Dictionary<Type, StateInfo>>();
_defaultGuildJson = PreloadDefaultGuildJson();
CreateDatabaseTablesAsync().Wait();
kb.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
kb.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
@ -48,31 +44,11 @@ namespace Kerobot.Services.GuildState
}
/// <summary>
/// Initializes guild in-memory and database structures, then attempts to load configuration.
/// Initializes guild in-memory structures and attempts to load configuration.
/// </summary>
private async Task InitializeGuild(SocketGuild arg)
{
// Get this done before any other thing.
await CreateSchema(arg.Id);
// Attempt initialization on the guild. All services will set up their tables here.
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(arg.Id))
{
foreach (var svc in Kerobot.Services)
{
try
{
await svc.CreateDatabaseTablesAsync(db);
}
catch (NpgsqlException ex)
{
await Log("Database error on CreateDatabaseTablesAsync:\n"
+ $"-- Service: {svc.Name}\n-- Guild: {arg.Id}\n-- Error: {ex.Message}", true);
}
}
}
// Then start loading guild information
// We're only loading config here now.
bool success = await LoadGuildConfiguration(arg.Id);
if (!success)
{
@ -118,6 +94,7 @@ namespace Kerobot.Services.GuildState
{
var jstr = await RetrieveConfiguration(guildId);
if (jstr == null) jstr = await RetrieveDefaultConfiguration();
int jstrHash = jstr.GetHashCode();
JObject guildConf;
try
@ -180,59 +157,47 @@ namespace Kerobot.Services.GuildState
}
#region Database
const string DBTableName = "guild_configuration";
/// <summary>
/// Creates a schema for holding all guild data.
/// Ensure that this runs first before any other database call to a guild.
/// Creates the table structures for holding guild configuration.
/// </summary>
private async Task CreateSchema(ulong guildId)
private async Task CreateDatabaseTablesAsync()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"create schema if not exists guild_{guildId}";
c.CommandText = $"create table if not exists {DBTableName} ("
+ $"rev_id SERIAL primary key, "
+ "guild_id bigint not null, "
+ "author bigint not null, "
+ "rev_date timestamptz not null default NOW(), "
+ "config_json text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
// Creating default configuration with revision ID 0.
// Config ID 0 is used when no other configurations can be loaded for a guild.
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {DBTableName} (rev_id, guild_id, author, config_json) "
+ "values (0, 0, 0, @Json) "
+ "on conflict (rev_id) do nothing";
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = PreloadDefaultGuildJson();
c.Prepare();
await c.ExecuteNonQueryAsync();
}
}
}
const string DBTableName = "guild_configuration";
/// <summary>
/// Creates the table structures for holding module configuration.
/// </summary>
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
{
using (var c = db.CreateCommand())
{
c.CommandText = $"create table if not exists {DBTableName} ("
+ $"rev_id SERIAL primary key, "
+ "author bigint not null, "
+ "rev_date timestamptz not null default NOW(), "
+ "config_json text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
// Creating default configuration with revision ID 0.
// This allows us to quickly define rev_id as type SERIAL and not have to configure it so that
// the serial should start at 2, but rather can easily start at 1. So lazy.
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {DBTableName} (rev_id, author, config_json)"
+ "values (0, 0, @Json) "
+ "on conflict (rev_id) do nothing";
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
c.Prepare();
await c.ExecuteNonQueryAsync();
}
}
private async Task<string> RetrieveConfiguration(ulong guildId)
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guildId))
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"select config_json from {DBTableName} "
c.CommandText = $"select config_json from {DBTableName} where guild_id = {guildId} "
+ "order by rev_id desc limit 1";
using (var r = await c.ExecuteReaderAsync())
{
@ -245,10 +210,25 @@ namespace Kerobot.Services.GuildState
}
}
}
private async Task<string> RetrieveDefaultConfiguration()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"select config_json from {DBTableName} where rev_id = 0";
using (var r = await c.ExecuteReaderAsync())
{
if (await r.ReadAsync()) return r.GetString(0);
}
}
}
throw new Exception("Unable to retrieve fallback configuration.");
}
#endregion
// Default guild configuration JSON is embedded in assembly. Retrieving and caching it here.
private readonly string _defaultGuildJson;
// Default guild configuration JSON is embedded in assembly.
private string PreloadDefaultGuildJson()
{
const string ResourceName = "Kerobot.DefaultGuildConfig.json";

View file

@ -1,13 +1,15 @@
using Discord;
using Npgsql;
using NpgsqlTypes;
using System;
using System;
using System.Threading.Tasks;
using Discord;
using NpgsqlTypes;
namespace Kerobot.Services.Logging
{
/// <summary>
/// Implements logging for the whole program.
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
/// Instance logs are messages of varying importance to the bot operator. Guild logs are messages that can be seen
/// by moderators of a particular guild. All log messages are backed by database.
/// Instance logs are stored as guild ID 0.
/// </summary>
class LoggingService : Service
{
@ -16,13 +18,8 @@ namespace Kerobot.Services.Logging
internal LoggingService(Kerobot kb) : base(kb)
{
// Create global instance log table
async Task CreateGlobalTable()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
await CreateDatabaseTablesAsync(db);
}
CreateGlobalTable().Wait();
// Create logging table
CreateDatabaseTablesAsync().Wait();
// Discord.Net log handling (client logging option is specified in Program.cs)
kb.DiscordClient.Log += DiscordClient_Log;
@ -34,7 +31,7 @@ namespace Kerobot.Services.Logging
/// <summary>
/// Discord.Net logging events handled here.
/// Only events with high severity are placed in the log. Others are just printed to console.
/// Only events with high importance are kept. Others are just printed to console.
/// </summary>
private async Task DiscordClient_Log(LogMessage arg)
{
@ -43,44 +40,54 @@ namespace Kerobot.Services.Logging
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
const string logSource = "Discord.Net";
if (important)
{
// Note: Using external method here!
await Kerobot.InstanceLogAsync(true, logSource, msg);
}
else
{
FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
}
if (important) await DoInstanceLogAsync(true, logSource, msg);
else FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
}
#region Database
const string TableLog = "program_log";
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
private async Task CreateDatabaseTablesAsync()
{
using (var c = db.CreateCommand())
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
c.CommandText = $"create table if not exists {TableLog} ("
+ "log_id serial primary key, "
+ "log_timestamp timestamptz not null, "
+ "log_source text not null, "
+ "message text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
}
private async Task TableInsertAsync(NpgsqlConnection db, DateTimeOffset timestamp, string source, string message)
{
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {TableLog} (log_timestamp, log_source, message) values"
+ "(@Ts, @Src, @Msg)";
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
c.Prepare();
await c.ExecuteNonQueryAsync();
using (var c = db.CreateCommand())
{
c.CommandText = $"create table if not exists {TableLog} ("
+ "log_id serial primary key, "
+ "guild_id bigint not null, "
+ "log_timestamp timestamptz not null, "
+ "log_source text not null, "
+ "message text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
using (var c = db.CreateCommand())
{
c.CommandText = "create index if not exists " +
$"{TableLog}_guildid_idx on {TableLog} guild_id";
await c.ExecuteNonQueryAsync();
}
}
}
private async Task TableInsertAsync(ulong guildId, DateTimeOffset timestamp, string source, string message)
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {TableLog} (guild_id, log_timestamp, log_source, message) values"
+ "(@Gid, @Ts, @Src, @Msg)";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
c.Prepare();
await c.ExecuteNonQueryAsync();
}
}
}
#endregion
/// <summary>
/// All console writes originate here.
@ -106,17 +113,15 @@ namespace Kerobot.Services.Logging
Exception insertException = null;
try
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
{
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
}
await TableInsertAsync(0, DateTimeOffset.UtcNow, source, message);
}
catch (Exception ex)
{
// This is not good. Resorting to plain console write to report the issue.
// Let's hope a warning reaches the reporting channel.
// Not good. Resorting to plain console write to report the error.
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
Console.WriteLine(ex.StackTrace);
// Attempt to pass this error to the reporting channel.
insertException = ex;
}
@ -175,14 +180,11 @@ namespace Kerobot.Services.Logging
{
try
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guild))
{
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
}
await TableInsertAsync(guild, DateTimeOffset.UtcNow, source, message);
}
catch (Exception ex)
{
// Probably a bad idea, but...
// This is probably a terrible idea, but...
await DoInstanceLogAsync(true, this.Name, "Failed to store guild log item: " + ex.Message);
// Stack trace goes to console only.
FormatToConsole(DateTime.UtcNow, this.Name, ex.StackTrace);

View file

@ -17,14 +17,6 @@ namespace Kerobot.Services
public Service(Kerobot kb) => Kerobot = kb;
/// <summary>
/// Initializes database tables per-guild.
/// This method is called by GuildStateService when entering a guild.
/// </summary>
/// <param name="db">An opened database connection with the appropriate schema option set.</param>
/// <remarks>If overriding, calling the base method is not necessary.</remarks>
public virtual Task CreateDatabaseTablesAsync(Npgsql.NpgsqlConnection db) => Task.CompletedTask;
/// <summary>
/// Creates a log message.
/// </summary>