Reverted "Reworked organization of Redis database logic."

Turns out Apache Kvrocks does not implement logical databases the same way as Redis. The SELECT command will accept any number and pretend to switch logical databases, but in reality it does not seem to do anything. Because of this, the original database logic is better suited. If a non-Redis database is used, such as Apache Kvrocks, the new database logic is actually a potential regression.
This commit is contained in:
2023-04-29 20:52:52 -04:00
parent 4a22c1abfc
commit ae259a5c1b
5 changed files with 126 additions and 228 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2023 Logan Fick * Copyright 2022 Logan Fick
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
* *
@@ -9,12 +9,13 @@
package dev.logal.crabstero; package dev.logal.crabstero;
import dev.logal.crabstero.listeners.*; import dev.logal.crabstero.listeners.*;
import dev.logal.crabstero.utils.RedisDatabase;
import org.javacord.api.DiscordApi; import org.javacord.api.DiscordApi;
import org.javacord.api.DiscordApiBuilder; import org.javacord.api.DiscordApiBuilder;
import org.javacord.api.entity.intent.Intent; import org.javacord.api.entity.intent.Intent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -26,7 +27,7 @@ public final class Crabstero {
private static final Logger logger = LoggerFactory.getLogger(Crabstero.class); private static final Logger logger = LoggerFactory.getLogger(Crabstero.class);
private final DiscordApi discordApi; private final DiscordApi discordApi;
private final RedisDatabase redisDatabase; private final JedisPool jedisPool;
private final ScheduledExecutorService workerPool; private final ScheduledExecutorService workerPool;
/** /**
@@ -36,12 +37,8 @@ public final class Crabstero {
* @param redisHost The host of the Redis server. * @param redisHost The host of the Redis server.
* @param redisPort The port of the Redis server. * @param redisPort The port of the Redis server.
*/ */
public Crabstero(final String token, final String redisHost, final Integer redisPort) { public Crabstero(final String token, final String redisHost, final int redisPort) {
if (redisPort == null) { this.jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort);
this.redisDatabase = new RedisDatabase(redisHost);
} else {
this.redisDatabase = new RedisDatabase(redisHost, redisPort);
}
this.workerPool = Executors.newScheduledThreadPool(4, new CrabsteroThreadFactory()); this.workerPool = Executors.newScheduledThreadPool(4, new CrabsteroThreadFactory());
final DiscordApiBuilder builder = new DiscordApiBuilder(); final DiscordApiBuilder builder = new DiscordApiBuilder();
@@ -75,14 +72,10 @@ public final class Crabstero {
public static void main(final String[] arguments) { public static void main(final String[] arguments) {
final String token = System.getenv("TOKEN"); final String token = System.getenv("TOKEN");
final String redisHost = System.getenv("REDIS_HOST"); final String redisHost = System.getenv("REDIS_HOST");
final String redisPort = System.getenv("REDIS_PORT"); final int redisPort = Integer.parseInt(System.getenv("REDIS_PORT"));
logger.info("Starting Crabstero..."); logger.info("Starting Crabstero...");
if (redisPort == null){ new Crabstero(token, redisHost, redisPort);
new Crabstero(token, redisHost, null);
} else {
new Crabstero(token, redisHost, Integer.parseInt(redisPort));
}
logger.info("Crabstero started!"); logger.info("Crabstero started!");
} }
@@ -96,12 +89,12 @@ public final class Crabstero {
} }
/** /**
* Gets the Redis database used by this instance of Crabstero. * Gets the Jedis pool used by this instance of Crabstero.
* *
* @return The Redis database. * @return The Jedis pool.
*/ */
public RedisDatabase getRedisDatabase() { public JedisPool getJedisPool() {
return this.redisDatabase; return this.jedisPool;
} }
/** /**

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2023 Logan Fick * Copyright 2022 Logan Fick
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
* *
@@ -10,11 +10,11 @@ package dev.logal.crabstero.tasks;
import dev.logal.crabstero.Crabstero; import dev.logal.crabstero.Crabstero;
import dev.logal.crabstero.utils.MarkovChainMessages; import dev.logal.crabstero.utils.MarkovChainMessages;
import dev.logal.crabstero.utils.RedisDatabase;
import org.javacord.api.entity.channel.ServerTextChannel; import org.javacord.api.entity.channel.ServerTextChannel;
import org.javacord.api.entity.message.Message; import org.javacord.api.entity.message.Message;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import java.util.Iterator; import java.util.Iterator;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -55,18 +55,19 @@ public final class ChannelHistoryIngestionTask implements Runnable {
return; return;
} }
try (final Jedis jedis = this.crabstero.getJedisPool().getResource()) {
// Have we already ingested this channel before? // Have we already ingested this channel before?
if (this.crabstero.getRedisDatabase().lrange(RedisDatabase.MAIN_DATABASE_ID, 0, INGESTED_CHANNELS_KEY, 0, -1).contains(this.channel.getIdAsString())) { if (jedis.lrange(INGESTED_CHANNELS_KEY, 0, -1).contains(this.channel.getIdAsString())) {
// Yes. Stop further processing of this channel. // Yes. Stop further processing of this channel.
return; return;
} else { } else {
// No. Add this channel's ID to the list of ingested channels. // No. Add this channel's ID to the list of ingested channels.
this.crabstero.getRedisDatabase().lpush(RedisDatabase.MAIN_DATABASE_ID, 0, INGESTED_CHANNELS_KEY, this.channel.getIdAsString()); jedis.lpush(INGESTED_CHANNELS_KEY, this.channel.getIdAsString());
// Log the start of the ingestion of this channel. // Log the start of the ingestion of this channel.
logger.info("Starting ingestion of text channel history. (Name: \"" + this.channel.getName() + "\" | ID: " + this.channel.getIdAsString() + " | Server ID: " + this.channel.getServer().getIdAsString() + ")"); logger.info("Starting ingestion of text channel history. (Name: \"" + this.channel.getName() + "\" | ID: " + this.channel.getIdAsString() + " | Server ID: " + this.channel.getServer().getIdAsString() + ")");
} }
}
final MarkovChainMessages markovChainMessages = new MarkovChainMessages(this.crabstero); final MarkovChainMessages markovChainMessages = new MarkovChainMessages(this.crabstero);
// Start streaming messages in this channel from newest to oldest. // Start streaming messages in this channel from newest to oldest.

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2023 Logan Fick * Copyright 2022 Logan Fick
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
* *
@@ -9,6 +9,8 @@
package dev.logal.crabstero.utils; package dev.logal.crabstero.utils;
import dev.logal.crabstero.Crabstero; import dev.logal.crabstero.Crabstero;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
@@ -18,11 +20,10 @@ import java.util.Random;
* Ingests sentences and generates new ones using a Markov chain with a Redis storage. * Ingests sentences and generates new ones using a Markov chain with a Redis storage.
*/ */
public final class MarkovChain { public final class MarkovChain {
private static final String STARTING_WORDS_KEY = "startingWords";
private static final char DEFAULT_SENTENCE_END = '§'; // Default character to end sentences with if it does not include a period, exclamation mark, or question mark. private static final char DEFAULT_SENTENCE_END = '§'; // Default character to end sentences with if it does not include a period, exclamation mark, or question mark.
private final long id; // A unique number for this Markov chain to segment it away from other Markov chains. This is usually the channel ID given by Discord. private final long id; // A unique number for this Markov chain to segment it away from other Markov chains. This is usually the channel ID given by Discord.
private final RedisDatabase redisDatabase; // The redis database for the Markov chain to store and retrieve data from. private final Crabstero crabstero; // The Crabstero instance using this Markov chain.
private final Random rng; // A source of randomness. private final Random rng; // A source of randomness.
/** /**
@@ -33,7 +34,7 @@ public final class MarkovChain {
*/ */
public MarkovChain(final long id, final Crabstero crabstero) { public MarkovChain(final long id, final Crabstero crabstero) {
this.id = id; this.id = id;
this.redisDatabase = crabstero.getRedisDatabase(); this.crabstero = crabstero;
this.rng = new SecureRandom(); // Probably placebo effect, but SecureRandom seems to produce better results. this.rng = new SecureRandom(); // Probably placebo effect, but SecureRandom seems to produce better results.
} }
@@ -46,7 +47,7 @@ public final class MarkovChain {
*/ */
public MarkovChain(final long id, final Crabstero crabstero, final Random random) { public MarkovChain(final long id, final Crabstero crabstero, final Random random) {
this.id = id; this.id = id;
this.redisDatabase = crabstero.getRedisDatabase(); this.crabstero = crabstero;
this.rng = random; this.rng = random;
} }
@@ -111,16 +112,22 @@ public final class MarkovChain {
// Finally, split the sentence into a string array with each element being a word. // Finally, split the sentence into a string array with each element being a word.
String[] words = sentence.trim().replaceAll(" +", " ").split(" "); String[] words = sentence.trim().replaceAll(" +", " ").split(" ");
// Get a reference to Jedis from the pool.
try (final Jedis jedis = this.crabstero.getJedisPool().getResource()) {
// Use a pipeline to avoid blocking caused by waiting for responses from the Redis server.
final Pipeline pipeline = jedis.pipelined();
// Iterate over the array of words, starting from the first element until the second to last element. // Iterate over the array of words, starting from the first element until the second to last element.
for (int i = 0; i < words.length - 1; i++) { for (int i = 0; i < words.length - 1; i++) {
// Is this the first word? // Is this the first word?
if (i == 0) { if (i == 0) {
// Yes. Push the word into a special list containing starting words. // Yes. Push the word into a special list containing starting words.
this.redisDatabase.lpush(RedisDatabase.MAIN_DATABASE_ID, this.id, STARTING_WORDS_KEY, words[i]); pipeline.lpush(this.id + ":start", words[i]);
} }
// Push the word after this one into a list identified by this word. // Push the word after this one into a list identified by this word.
this.redisDatabase.lpush(RedisDatabase.WORDS_DATABASE_ID, this.id, words[i], words[i + 1]); pipeline.lpush(this.id + "::" + words[i], words[i + 1]);
}
} }
} }
@@ -135,9 +142,12 @@ public final class MarkovChain {
// Declare a new builder for building the new sentence. // Declare a new builder for building the new sentence.
final StringBuilder newSentence = new StringBuilder(); final StringBuilder newSentence = new StringBuilder();
// Get a reference to Jedis from the pool.
try (final Jedis jedis = this.crabstero.getJedisPool().getResource()) {
// Does this Markov chain have any starting words? // Does this Markov chain have any starting words?
if (!this.redisDatabase.exists(RedisDatabase.MAIN_DATABASE_ID, this.id, STARTING_WORDS_KEY)) { if (!jedis.exists(this.id + ":start")) {
// No. Quickly ingest something to avoid throwing an error. // No. Quickly ingest something to avoid throwing an error.
// TODO: Could this soft lock since a reference to the jedis pool is already held?
this.ingestSentence("Hello world!"); this.ingestSentence("Hello world!");
} }
@@ -145,7 +155,7 @@ public final class MarkovChain {
String word; String word;
// Get a list of all starting words. // Get a list of all starting words.
final List<String> startingWords = this.redisDatabase.lrange(RedisDatabase.MAIN_DATABASE_ID, this.id, STARTING_WORDS_KEY, 0, 499); // TODO: What's a good stop value? 0 seems like a potential DoS vector. final List<String> startingWords = jedis.lrange(this.id + ":start", 0, -1);
// Pick a random index for a starting word. // Pick a random index for a starting word.
int index = rng.nextInt(startingWords.size()); int index = rng.nextInt(startingWords.size());
@@ -160,7 +170,7 @@ public final class MarkovChain {
// Although quite rare, this loop will skip if the starting word is already complete on its own (e.g. "Yes.") // Although quite rare, this loop will skip if the starting word is already complete on its own (e.g. "Yes.")
while (!isCompleteSentence(word)) { while (!isCompleteSentence(word)) {
// Get the list of words which can be added after the previous word. // Get the list of words which can be added after the previous word.
final List<String> wordChoices = this.redisDatabase.lrange(RedisDatabase.WORDS_DATABASE_ID, this.id, word, 0, 499); // TODO: What's a good stop value? 0 seems like a potential DoS vector. final List<String> wordChoices = jedis.lrange(this.id + "::" + word, 0, -1);
// Reset the index. // Reset the index.
index = -1; index = -1;
@@ -207,6 +217,7 @@ public final class MarkovChain {
break; break;
} }
} }
}
// Is the last character of the new sentence the default sentence end character? // Is the last character of the new sentence the default sentence end character?
if (newSentence.charAt(newSentence.length() - 1) == DEFAULT_SENTENCE_END) { if (newSentence.charAt(newSentence.length() - 1) == DEFAULT_SENTENCE_END) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2023 Logan Fick * Copyright 2022 Logan Fick
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
* *
@@ -18,6 +18,7 @@ import org.javacord.api.entity.message.embed.EmbedBuilder;
import org.javacord.api.entity.message.mention.AllowedMentions; import org.javacord.api.entity.message.mention.AllowedMentions;
import org.javacord.api.entity.message.mention.AllowedMentionsBuilder; import org.javacord.api.entity.message.mention.AllowedMentionsBuilder;
import org.javacord.api.util.logging.ExceptionLogger; import org.javacord.api.util.logging.ExceptionLogger;
import redis.clients.jedis.Jedis;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
@@ -27,8 +28,6 @@ import java.util.Random;
* Assists with generating Discord messages in response to other users and ingesting raw messages. * Assists with generating Discord messages in response to other users and ingesting raw messages.
*/ */
public class MarkovChainMessages { public class MarkovChainMessages {
private static final String IMAGE_URLS_KEY = "imageURLS";
private final Crabstero crabstero; private final Crabstero crabstero;
private final AllowedMentions allowedMentions; private final AllowedMentions allowedMentions;
private final Random rng = new SecureRandom(); private final Random rng = new SecureRandom();
@@ -86,11 +85,13 @@ public class MarkovChainMessages {
embed.setDescription(markovChain.generate(300, 500)); embed.setDescription(markovChain.generate(300, 500));
// If image URLs are known for this channel, chose a random one and attach to the embed. // If image URLs are known for this channel, chose a random one and attach to the embed.
final List<String> embedImageURLs = this.crabstero.getRedisDatabase().lrange(RedisDatabase.MAIN_DATABASE_ID, channelID, IMAGE_URLS_KEY, 0, 499); // TODO: What's a good stop value? 0 seems like a potential DoS vector. try (final Jedis jedis = this.crabstero.getJedisPool().getResource()) {
final List<String> embedImageURLs = jedis.lrange(channelID + ":images", 0, -1);
if (embedImageURLs.size() > 0) { if (embedImageURLs.size() > 0) {
embed.setImage(embedImageURLs.get(this.rng.nextInt(embedImageURLs.size()))); embed.setImage(embedImageURLs.get(this.rng.nextInt(embedImageURLs.size())));
} }
}
// Add a watermark to the footer of the embed. // Add a watermark to the footer of the embed.
embed.setFooter("Crabstero is a logal.dev project", "https://logal.dev/images/logo.png"); embed.setFooter("Crabstero is a logal.dev project", "https://logal.dev/images/logo.png");
@@ -148,7 +149,9 @@ public class MarkovChainMessages {
// If the embed has an image, store the URL to the image. // If the embed has an image, store the URL to the image.
embed.getImage().ifPresent((image) -> { embed.getImage().ifPresent((image) -> {
this.crabstero.getRedisDatabase().lpush(RedisDatabase.MAIN_DATABASE_ID, channelID, IMAGE_URLS_KEY, image.getUrl().toString()); try (final Jedis jedis = this.crabstero.getJedisPool().getResource()) {
jedis.lpush(channelID + ":images", image.getUrl().toString());
}
}); });
} }
} }

View File

@@ -1,110 +0,0 @@
/*
* Copyright 2023 Logan Fick
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
package dev.logal.crabstero.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.List;
/**
* Wrapper for a Redis database which forces logical separation of data by requiring two extra parameters for every command:
* 1. A logical database ID, which is intended to separate Markov chain data from other miscellaneous state data.
* 2. A general unique ID, which is intended to separate data in the same logical database between Discord channels.
*/
public final class RedisDatabase {
public static final int MAIN_DATABASE_ID = 0; // The ID of the logical database which stores miscellaneous state data.
public static final int WORDS_DATABASE_ID = 1; // The ID of the logical database which specifically stores Markov chain data.
private static final int DEFAULT_REDIS_PORT = 6379; // Default port to use when not specifically during instantiation.
private static final String SEPARATOR = ":"; // Separator to use between the virtual namespace ID and key.
private final JedisPool jedisPool;
/**
* Creates a new Redis database which connects to the Redis server hosted on the given host and port DEFAULT_REDIS_PORT.
*
* @param redisHost The host of the Redis server.
*/
public RedisDatabase(final String redisHost) {
this.jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, DEFAULT_REDIS_PORT);
}
/**
* Creates a new Redis database which connects to the Redis server hosted on the given host and port.
*
* @param redisHost The host of the Redis server.
* @param redisPort The port of the Redis server.
*/
public RedisDatabase(final String redisHost, final int redisPort) {
this.jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort);
}
/**
* Converts a given unique ID and a key into a single String which can be used as a key to the backend Redis database.
*
* @param id The unique ID.
* @param key The key.
* @return The long key which can be sent to a Redis database, unless the given unique ID is 0, then just key exactly as given.
*/
private static String resolveKey(final long id, final String key) {
if (id == 0) {
return key;
} else {
return id + SEPARATOR + key;
}
}
/**
* Adds a string to the head of the list stored at the given key uniquely identified by the given ID in the given logical database ID.
*
* @param database Zero-based index logical database ID.
* @param id The unique ID.
* @param key The key of the list.
* @param values The value(s) to add to the list.
*/
public void lpush(final int database, final long id, final String key, final String... values) {
try (final Jedis jedis = this.jedisPool.getResource()) {
jedis.select(database);
jedis.lpush(resolveKey(id, key), values);
}
}
/**
* Tests whether the given key uniquely identified by the given ID in the given logical database ID exists.
*
* @param database Zero-based index logical database ID.
* @param id The unique ID.
* @param key The key to test.
* @return True if the key exists, false otherwise.
*/
public boolean exists(final int database, final long id, final String key) {
try (final Jedis jedis = this.jedisPool.getResource()) {
jedis.select(database);
return jedis.exists(resolveKey(id, key));
}
}
/**
* Returns the elements starting and ending at the given indexes of the list stored at the given key uniquely identified by the given ID in the given logical database ID.
*
* @param database Zero-based index logical database ID.
* @param id The unique ID
* @param key The key of the list.
* @param start Zero-based index of the starting element.
* @param stop Zero-based index of the ending element.
* @return A List with the requested elements.
*/
public List<String> lrange(final int database, final long id, final String key, final long start, final long stop) {
try (final Jedis jedis = this.jedisPool.getResource()) {
jedis.select(database);
return jedis.lrange(resolveKey(id, key), start, stop);
}
}
}