From 890e7e5ce5f91ca0878ebf8a01a67978a172ebfa Mon Sep 17 00:00:00 2001
From: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com>
Date: Mon, 20 Sep 2021 23:50:51 +0100
Subject: [PATCH] I think this is instance communication
---
.github/CODEOWNERS | 1 +
SQL/paradise_schema.sql | 14 +-
SQL/updates/25-26.sql | 11 ++
code/__DEFINES/misc.dm | 2 +-
.../configuration/configuration_core.dm | 4 -
.../sections/instancing_configuration.dm | 20 ---
.../sections/system_configuration.dm | 7 +
code/controllers/subsystem/instancing.dm | 165 ++++++++++++------
code/game/world.dm | 1 +
code/modules/admin/admin_verbs.dm | 1 -
code/modules/admin/verbs/view_instances.dm | 12 +-
code/modules/client/client_procs.dm | 5 +
code/modules/world_topic/instance_announce.dm | 9 +-
code/modules/world_topic/server_discovery.dm | 12 --
config/example/config.toml | 24 +--
paradise.dme | 2 -
16 files changed, 163 insertions(+), 127 deletions(-)
create mode 100644 SQL/updates/25-26.sql
delete mode 100644 code/controllers/configuration/sections/instancing_configuration.dm
delete mode 100644 code/modules/world_topic/server_discovery.dm
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ccd774cdaa8..894a16ac470 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -5,6 +5,7 @@
### AffectedArc07
# Actual Code
+/code/controllers/subsystem/instancing.dm @AffectedArc07
/code/controllers/subsystem/mapping.dm @AffectedArc07
/code/controllers/subsystem/ticker.dm @AffectedArc07
/tgui/ @AffectedArc07
diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql
index 3ed1cbca688..eb0deeb93e2 100644
--- a/SQL/paradise_schema.sql
+++ b/SQL/paradise_schema.sql
@@ -597,5 +597,15 @@ CREATE TABLE `2fa_secrets` (
`date_setup` DATETIME NOT NULL DEFAULT current_timestamp(),
`last_time` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`ckey`) USING BTREE
-)
-COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
+) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
+
+--
+-- Table structure for table `instance_data_cache`
+--
+CREATE TABLE `instance_data_cache` (
+ `server_id` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `key_name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `key_value` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `last_updated` TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`server_id`, `key_name`) USING HASH
+) COLLATE='utf8mb4_general_ci' ENGINE=MEMORY;
diff --git a/SQL/updates/25-26.sql b/SQL/updates/25-26.sql
new file mode 100644
index 00000000000..c1e3834ab8d
--- /dev/null
+++ b/SQL/updates/25-26.sql
@@ -0,0 +1,11 @@
+# Updates DB from 25 to 26 -AffectedArc07
+# Adds a new table for instance data caching, and server_id fields on other tables
+# NOTE: If this line makes it into the final PR I will be upset with myself
+
+CREATE TABLE `instance_data_cache` (
+ `server_id` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `key_name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `key_value` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `last_updated` TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`server_id`, `key_name`) USING HASH
+) COLLATE='utf8mb4_general_ci' ENGINE=MEMORY;
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 2b95577ef3c..e16635d1c07 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -365,7 +365,7 @@
#define INVESTIGATE_BOMB "bombs"
// The SQL version required by this version of the code
-#define SQL_VERSION 25
+#define SQL_VERSION 26
// Vending machine stuff
#define CAT_NORMAL 1
diff --git a/code/controllers/configuration/configuration_core.dm b/code/controllers/configuration/configuration_core.dm
index ad2cf3c5bda..81c2c6787bf 100644
--- a/code/controllers/configuration/configuration_core.dm
+++ b/code/controllers/configuration/configuration_core.dm
@@ -24,8 +24,6 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
var/datum/configuration_section/gateway_configuration/gateway
/// Holder for the general configuration datum
var/datum/configuration_section/general_configuration/general
- /// Holder for the instancing configuration datum
- var/datum/configuration_section/instancing_configuration/instancing
/// Holder for the IPIntel configuration datum
var/datum/configuration_section/ipintel_configuration/ipintel
/// Holder for the job configuration datum
@@ -83,7 +81,6 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
gamemode = new()
gateway = new()
general = new()
- instancing = new()
ipintel = new()
jobs = new()
logging = new()
@@ -122,7 +119,6 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
safe_load(gamemode, "gamemode_configuration")
safe_load(gateway, "gateway_configuration")
safe_load(general, "general_configuration")
- safe_load(instancing, "instancing_configuration")
safe_load(ipintel, "ipintel_configuration")
safe_load(jobs, "job_configuration")
safe_load(logging, "logging_configuration")
diff --git a/code/controllers/configuration/sections/instancing_configuration.dm b/code/controllers/configuration/sections/instancing_configuration.dm
deleted file mode 100644
index bc5c6c8406a..00000000000
--- a/code/controllers/configuration/sections/instancing_configuration.dm
+++ /dev/null
@@ -1,20 +0,0 @@
-/// Config holder for stuff relating to multi-server instances
-/datum/configuration_section/instancing_configuration
- /// ID of this specific server
- var/server_id = "paradise_main"
- /// List of all peer servers
- var/list/datum/peer_server/peers = list()
-
-/datum/configuration_section/instancing_configuration/load_data(list/data)
- // Use the load wrappers here. That way the default isnt made 'null' if you comment out the config line
- CONFIG_LOAD_STR(server_id, data["server_id"])
-
- if(islist(data["peer_servers"]))
- for(var/list/server in data["peer_servers"])
- if(server["server_port"] == world.port) // Skip our own instance
- continue
- var/datum/peer_server/PS = new()
- PS.internal_ip = server["internal_ip"]
- PS.server_port = server["server_port"]
- PS.commskey = server["commskey"]
- peers.Add(PS)
diff --git a/code/controllers/configuration/sections/system_configuration.dm b/code/controllers/configuration/sections/system_configuration.dm
index 42e563d7057..d4d4a3fe28c 100644
--- a/code/controllers/configuration/sections/system_configuration.dm
+++ b/code/controllers/configuration/sections/system_configuration.dm
@@ -16,6 +16,10 @@
var/_2fa_auth_host = null
/// List of IP addresses which bypass world topic rate limiting
var/list/topic_ip_ratelimit_bypass = list()
+ /// Server instance ID
+ var/instance_id = "paradise_main"
+ /// Server internal IP
+ var/internal_ip = "127.0.0.1"
/datum/configuration_section/system_configuration/load_data(list/data)
// Use the load wrappers here. That way the default isnt made 'null' if you comment out the config line
@@ -28,3 +32,6 @@
CONFIG_LOAD_STR(_2fa_auth_host, data["_2fa_auth_host"])
CONFIG_LOAD_LIST(topic_ip_ratelimit_bypass, data["topic_ip_ratelimit_bypass"])
+
+ CONFIG_LOAD_STR(instance_id, data["instance_id"])
+ CONFIG_LOAD_STR(internal_ip, data["internal_ip"])
diff --git a/code/controllers/subsystem/instancing.dm b/code/controllers/subsystem/instancing.dm
index 87ad3f06e7c..30214e010a8 100644
--- a/code/controllers/subsystem/instancing.dm
+++ b/code/controllers/subsystem/instancing.dm
@@ -1,84 +1,124 @@
SUBSYSTEM_DEF(instancing)
name = "Instancing"
runlevels = RUNLEVEL_INIT | RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ wait = 30 SECONDS
+ flags = SS_KEEP_TIMING
/// Has our initial check complete? Used to halt init but not lag the server
var/initial_check_complete = FALSE
/// Is a check currently running?
var/check_running = FALSE
/datum/controller/subsystem/instancing/Initialize(start_timeofday)
- // Do an initial peer check
- check_peers(TRUE) // Force because of time memes
- UNTIL(initial_check_complete) // Wait here a bit
+ // Dont even bother if we arent connected
+ if(!SSdbcore.IsConnected())
+ flags |= SS_NO_FIRE
+ return ..()
+ update_heartbeat() // Make sure you do this before announcing to peers, or no one will hear your announcement
var/startup_msg = "The server [GLOB.configuration.general.server_name] is now starting up. The map is [SSmapping.map_datum.fluff_name] ([SSmapping.map_datum.technical_name]). You can connect with the Switch Server verb."
- message_all_peers(startup_msg, send_reping = TRUE)
+ message_all_peers(startup_msg)
return ..()
/datum/controller/subsystem/instancing/fire(resumed)
- check_peers()
+ update_heartbeat()
+ update_playercache()
/**
- * Refreshes all peers on the server
+ * Playercache updater
*
- * Called periodically during fire() as well as when a new peer reports itself as online
- * Only one instance of this proc may run at a time
- *
- * Arguments:
- * * force - Do we want to force check all of them
+ * Updates the player cache in the DB. Different from heartbeat so we can force invoke it on player operations
*/
-/datum/controller/subsystem/instancing/proc/check_peers(force = FALSE)
- set waitfor = FALSE // This has sleeps if it cant topic so we dont want to bog things down
- if(check_running)
+/datum/controller/subsystem/instancing/proc/update_playercache(optional_ckey)
+ // You may be wondering, why the fuck is an "optional ckey" variable here
+ // Well, this is invoked in client/New(), and needs to read from GLOB.clients
+ // However, this proc sleeps, and if you sleep during client/New() once the client is in GLOB.clients, stuff breaks bad
+ // (See my comment rambling in client/New())
+ // By passing the ckey through, we can sleep in this proc and still get the data
+ if(!SSdbcore.IsConnected())
return
+ // First iterate clients to get ckeys
+ var/list/ckeys = list()
+ for(var/client/C in GLOB.clients) // No code review. I am not doing the `as anything` bullshit, because we *do* need the type checks here to avoid null clients which do happen sometimes
+ ckeys += C.ckey
+ // Add our optional
+ if(optional_ckey)
+ ckeys += optional_ckey
+ // Note: We dont have to sort the list here. The only time this is read for is a search,
+ // and since BYOND lists are linked lists internally, order doesnt matter
+ var/ckey_json = json_encode(ckeys)
- check_running = TRUE // Dont allow multiple of these
- // A NOTE TO ANYONE ELSE WHO LOOKS AT THIS
- // THESE TIMINGS ARE A FUCKING NIGHTMARE AND YOU WILL NEED DEBUG LOGGING TO TEST THEM
- // DO NOT FUCK WITH THE TIMINGS -aa
- for(var/datum/peer_server/PS in GLOB.configuration.instancing.peers)
- // If the server hasnt been discovered and its been more than 5 minutes
- if((!PS.discovered && PS.last_operation_time + 5 MINUTES > world.time))
- if(!force) // No, code review. This is not going in the same line.
- continue
+ // Yes I care about performance savings this much here to mass execute this shit
+ var/list/datum/db_query/queries = list()
+ queries += SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=:json WHERE key_name='playerlist' AND server_id=:sid", list(
+ "json" = ckey_json,
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ queries += SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=:count WHERE key_name='playercount' AND server_id=:sid", list(
+ "count" = length(ckeys),
+ "sid" = GLOB.configuration.system.instance_id
+ ))
- // Only run main operations once every minute anyway
- if(PS.last_operation_time + 1 MINUTES > world.time)
- if(!force)
- continue
+ SSdbcore.MassExecute(queries, TRUE, TRUE, FALSE, FALSE)
- var/peer_response = world.Export("byond://[PS.internal_ip]:[PS.server_port]?server_discovery&key=[PS.commskey]")
- if(!peer_response)
- PS.online = FALSE // Peer is offline
- PS.last_operation_time = world.time
- continue
+/**
+ * Heartbeat updater
+ *
+ * Updates the heartbeat in the DB. Used so other servers can see when this one was alive
+ */
+/datum/controller/subsystem/instancing/proc/update_heartbeat()
+ // this could probably just go in fire() but clean code and profiler ease who cares
+ var/datum/db_query/dbq = SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=NOW() WHERE key_name='heartbeat' AND server_id=:sid", list(
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ dbq.warn_execute()
+ qdel(dbq)
- // We got a response
- PS.discovered = TRUE
- PS.online = TRUE
- var/list/peer_data = json_decode(peer_response)
+/**
+ * Seed data
+ *
+ * Seeds all our data into the DB for other servers to discover from.
+ * This is called during world/New() instead of on initialize so it can be done *instantly*
+ */
+/datum/controller/subsystem/instancing/proc/seed_data()
+ // We need to seed a lot of keys, so lets just use a key-value-pair-map to do this easily
+ var/list/kvp_map = list()
+ kvp_map["server_name"] = GLOB.configuration.general.server_name // Name of the server
+ kvp_map["server_port"] = world.port // Server port (used for redirection and topics)
+ kvp_map["topic_key"] = GLOB.configuration.system.topic_key // Server topic key (used for topics)
+ kvp_map["internal_ip"] = GLOB.configuration.system.internal_ip // Server internal IP (used for topics)
+ kvp_map["playercount"] = length(GLOB.clients) // Server client count (used for status info)
+ kvp_map["playerlist"] = json_encode(list()) // Server client list. Used for dupe login checks. This gets filled in later
+ kvp_map["heartbeat"] = SQLtime() // SQL timestamp for heartbeat purposes. Any server without a heartbeat in the last 60 seconds can be considered dead
+ // Also note for above. You may say "But AA you dont need to JSON encode it, just use "\[]"."
+ // Well to that I say, no. This is meant to be JSON regardless, and it should represent that. This proc is ran once during world/New()
+ // An extra nanosecond of load will make zero difference.
- PS.external_ip = peer_data["external_ip"]
- PS.server_id = peer_data["server_id"]
- PS.server_name = peer_data["server_name"]
- PS.playercount = peer_data["playercount"]
+ for(var/key in kvp_map)
+ var/datum/db_query/dbq = SSdbcore.NewQuery("INSERT INTO instance_data_cache (server_id, key_name, key_value) VALUES (:sid, :kn, :kv) ON DUPLICATE KEY UPDATE key_value=:kv2", // Is this necessary? Who knows!
+ list(
+ "sid" = GLOB.configuration.system.instance_id,
+ "kn" = key,
+ "kv" = "[kvp_map[key]]", // String encoding IS necessary since these tables use strings, not ints
+ "kv2" = "[kvp_map[key]]", // Dont know if I need the second but better to be safe
+ )
+ )
+ dbq.warn_execute(FALSE) // Do NOT async execute here because world/New() shouldnt sleep. EVER. You get issues if you do.
+ qdel(dbq)
- PS.last_operation_time = world.time
-
- check_running = FALSE
- initial_check_complete = TRUE
/**
* Message all peers
*
- * Wrapper for [topic_all_peers] to autoformat a message topic. Will send a server-wide announcement to the other servers
- * including any relevant detail
+ * Wrapper for [topic_all_peers] to format the input into a message topic. Will send a server-wide announcement to the other servers
+ *
* Arguments:
* * message - Message to send to the other servers
* * include_offline - Whether to topic offline servers on the off chance they came online
*/
-/datum/controller/subsystem/instancing/proc/message_all_peers(message, include_offline = FALSE, send_reping = FALSE)
+/datum/controller/subsystem/instancing/proc/message_all_peers(message)
+ if(!SSdbcore.IsConnected())
+ return
var/topic_string = "instance_announce&msg=[html_encode(message)]"
- topic_all_peers(topic_string, include_offline, send_reping)
+ topic_all_peers(topic_string)
/**
* Sends a topic to all peers
@@ -89,7 +129,28 @@ SUBSYSTEM_DEF(instancing)
* * raw_topic - The raw topic to send to the other servers
* * include_offline - Whether to topic offline servers on the off chance they came online
*/
-/datum/controller/subsystem/instancing/proc/topic_all_peers(raw_topic, include_offline = FALSE, send_reping = FALSE)
- for(var/datum/peer_server/PS in GLOB.configuration.instancing.peers)
- if(PS.online || include_offline)
- world.Export("byond://[PS.internal_ip]:[PS.server_port]?[raw_topic]&key=[PS.commskey][send_reping ? "&repoll=1" : ""]")
+/datum/controller/subsystem/instancing/proc/topic_all_peers(raw_topic)
+ // Someone here is going to say "AA you shouldnt put load on the DB server you can do sorting in BYOND"
+ // Well let me put it this way. The DB server is an entirely different machine to BYOND,w ith this entire dataset being stored in its RAM, not even on disk
+ // By making the DB server do the work, we can offload from BYOND, which is already strained
+ var/datum/db_query/dbq1 = SSdbcore.NewQuery({"
+ SELECT server_id, key_name, key_value FROM instance_data_cache WHERE server_id IN
+ (SELECT server_id FROM instance_data_cache WHERE server_id !=:sid AND
+ key_name='heartbeat' AND last_updated BETWEEN NOW() - INTERVAL 60 SECOND AND NOW())
+ AND key_name IN ("topic_key", "internal_ip", "server_port")"}, list(
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ if(!dbq1.warn_execute())
+ qdel(dbq1)
+ return
+
+ var/servers_outer = list()
+ while(dbq1.NextRow())
+ if(!servers_outer[dbq1.item[1]])
+ servers_outer[dbq1.item[1]] = list()
+
+ servers_outer[dbq1.item[1]][dbq1.item[2]] = dbq1.item[3] // This should assoc load our data
+
+ for(var/server in servers_outer)
+ var/server_data = servers_outer[server]
+ world.Export("byond://[server_data["internal_ip"]]:[server_data["server_port"]]?[raw_topic]&key=[server_data["topic_key"]]")
diff --git a/code/game/world.dm b/code/game/world.dm
index 2153a5517b5..e8d048c2d4e 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -30,6 +30,7 @@ GLOBAL_LIST_INIT(map_transition_config, list(CC_TRANSITION_CONFIG))
// Right off the bat, load up the DB
SSdbcore.CheckSchemaVersion() // This doesnt just check the schema version, it also connects to the db! This needs to happen super early! I cannot stress this enough!
SSdbcore.SetRoundID() // Set the round ID here
+ SSinstancing.seed_data() // Set us up in the DB
// Setup all log paths and stamp them with startups, including round IDs
SetupLogs()
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 2a1d77da5e1..3768ccbf833 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -139,7 +139,6 @@ GLOBAL_LIST_INIT(admin_verbs_server, list(
/client/proc/set_ooc,
/client/proc/reset_ooc,
/client/proc/set_next_map,
- /client/proc/refresh_instances,
/client/proc/manage_queue,
/client/proc/add_queue_server_bypass
))
diff --git a/code/modules/admin/verbs/view_instances.dm b/code/modules/admin/verbs/view_instances.dm
index 3cedf9614a0..bbef0b184dc 100644
--- a/code/modules/admin/verbs/view_instances.dm
+++ b/code/modules/admin/verbs/view_instances.dm
@@ -7,6 +7,8 @@
return
to_chat(usr, "Server instances info")
+ to_chat(usr, "AA you need to finish this you lazy oaf")
+ /*
for(var/datum/peer_server/PS in GLOB.configuration.instancing.peers)
// We havnt even been discovered, so we cant even be online
if(!PS.discovered)
@@ -20,13 +22,5 @@
// If we are here, we are online, so we can do a rich report
to_chat(usr, "ID [PS.server_id] - ONLINE (Players: [PS.playercount])")
+ */
-/client/proc/refresh_instances()
- set name = "Force Refresh Server Instances"
- set desc = "Force refresh the local cache of server instances"
- set category = "Server"
-
- if(!check_rights(R_SERVER))
- return
-
- SSinstancing.check_peers(TRUE) // Force check
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index c841c759229..9e7858e20b1 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -326,12 +326,16 @@
if(length(related_accounts_cid))
log_admin("[key_name(src)] Alts by CID: [jointext(related_accounts_cid, " ")]")
+ // This sleeps so it has to go here. Dont fucking move it.
+ SSinstancing.update_playercache(ckey)
+
// This has to go here to avoid issues
// If you sleep past this point, you will get SSinput errors as well as goonchat errors
// DO NOT STUFF RANDOM SQL QUERIES BELOW THIS POINT WITHOUT USING `INVOKE_ASYNC()` OR SIMILAR
// YOU WILL BREAK STUFF. SERIOUSLY. -aa07
GLOB.clients += src
+
spawn() // Goonchat does some non-instant checks in start()
chatOutput.start()
@@ -434,6 +438,7 @@
GLOB.admins -= src
GLOB.directory -= ckey
GLOB.clients -= src
+ SSinstancing.update_playercache() // Clear us out
QDEL_NULL(chatOutput)
if(movingmob)
movingmob.client_mobs_in_contents -= mob
diff --git a/code/modules/world_topic/instance_announce.dm b/code/modules/world_topic/instance_announce.dm
index 250046cd53b..d4ecaa1ba85 100644
--- a/code/modules/world_topic/instance_announce.dm
+++ b/code/modules/world_topic/instance_announce.dm
@@ -3,12 +3,5 @@
requires_commskey = TRUE
/datum/world_topic_handler/instance_announce/execute(list/input, key_valid)
- var/msg = input["msg"]
- if(input["repoll"]) // Repoll peers
- UNTIL(!SSinstancing.check_running) // If we are running, wait
- SSinstancing.check_peers(TRUE) // Manually check, with FORCE
- UNTIL(!SSinstancing.check_running) // Wait for completion
-
- // Now that we repolled, we can tell the playerbase
- to_chat(world, "