Implemented selective loading/unloading of worlds. Summary of changes:

1. Database is now iteratively updated, rather than dropped and recreated every time the universe is saved.
2. A single connection to the database is opened at server creation time, and used throughout.
3. Worlds, users and server information are now stored with appropriate IDs, so that they can be suitably updated. The world ID can be computed from world coordinates alone.
4. After saving, a world is unloaded from memory if it doesn't contain any updatable objects, and if all of its neighbours are either:  (i) uncharted; (ii) not loaded in memory; (iii) without any updatable objects. This ensures that world unloading/reloading cannot be spammed by a single bot repeatedly transitioning in/out of an otherwise empty world.
5. The instance method GameUniverse.getWorld(int,int,boolean) first checks the world with given coordinates is in memory, then tries to load it from database, and only then creates a new one (if required by the boolean flag).
6. Worlds are now stored in a Hashtable indexed by world ID, for faster retrieval.
This commit is contained in:
sg495 2018-01-08 18:23:10 +01:00
parent 811a443a4e
commit 593be7624e
4 changed files with 307 additions and 113 deletions

View File

@ -36,11 +36,20 @@ public class GameServer implements Runnable {
private DayNightCycle dayNightCycle; private DayNightCycle dayNightCycle;
private MongoClient mongo = null;
public GameServer() { public GameServer() {
this.config = new ServerConfiguration("config.properties"); this.config = new ServerConfiguration("config.properties");
try{
mongo = new MongoClient("localhost", 27017);
} catch (UnknownHostException e) {
e.printStackTrace();
}
gameUniverse = new GameUniverse(config); gameUniverse = new GameUniverse(config);
gameUniverse.setMongo(mongo);
pluginManager = new PluginManager(); pluginManager = new PluginManager();
maxExecutionTime = config.getInt("user_timeout"); maxExecutionTime = config.getInt("user_timeout");
@ -169,12 +178,11 @@ public class GameServer implements Runnable {
") updated"); ") updated");
} }
void load() { void load() {
LogManager.LOGGER.info("Loading from MongoDB"); LogManager.LOGGER.info("Loading all data from MongoDB");
MongoClient mongo;
try {
mongo = new MongoClient("localhost", 27017);
DB db = mongo.getDB("mar"); DB db = mongo.getDB("mar");
@ -182,17 +190,20 @@ public class GameServer implements Runnable {
DBCollection users = db.getCollection("user"); DBCollection users = db.getCollection("user");
DBCollection server = db.getCollection("server"); DBCollection server = db.getCollection("server");
//Load worlds BasicDBObject whereQuery = new BasicDBObject();
DBCursor cursor = worlds.find(); whereQuery.put("shouldUpdate", true);
DBCursor cursor = worlds.find(whereQuery);
GameUniverse universe = GameServer.INSTANCE.getGameUniverse();
while (cursor.hasNext()) { while (cursor.hasNext()) {
GameServer.INSTANCE.getGameUniverse().getWorlds().add(World.deserialize(cursor.next())); World w = World.deserialize(cursor.next());
universe.addWorld(w);
} }
//Load users //Load users
cursor = users.find(); cursor = users.find();
while (cursor.hasNext()) { while (cursor.hasNext()) {
try { try {
GameServer.INSTANCE.getGameUniverse().getUsers().add(User.deserialize(cursor.next())); universe.getUsers().add(User.deserialize(cursor.next()));
} catch (CancelledException e) { } catch (CancelledException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -208,22 +219,15 @@ public class GameServer implements Runnable {
LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorlds().size() + LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorlds().size() +
" | U:" + GameServer.INSTANCE.getGameUniverse().getUsers().size()); " | U:" + GameServer.INSTANCE.getGameUniverse().getUsers().size());
} catch (UnknownHostException e) {
e.printStackTrace();
}
} }
private void save() { private void save() {
LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorlds().size() + " | U:" + gameUniverse.getUsers().size()); LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorlds().size() + " | U:" + gameUniverse.getUsers().size());
MongoClient mongo;
try {
mongo = new MongoClient("localhost", 27017);
DB db = mongo.getDB("mar"); DB db = mongo.getDB("mar");
db.dropDatabase(); //Todo: Update database / keep history instead of overwriting int unloaded_worlds = 0;
DBCollection worlds = db.getCollection("world"); DBCollection worlds = db.getCollection("world");
DBCollection users = db.getCollection("user"); DBCollection users = db.getCollection("user");
@ -232,14 +236,18 @@ public class GameServer implements Runnable {
List<DBObject> worldDocuments = new ArrayList<>(); List<DBObject> worldDocuments = new ArrayList<>();
int perBatch = 35; int perBatch = 35;
int insertedWorlds = 0; int insertedWorlds = 0;
ArrayList<World> worlds_ = new ArrayList<>(GameServer.INSTANCE.getGameUniverse().getWorlds()); GameUniverse universe = GameServer.INSTANCE.getGameUniverse();
ArrayList<World> worlds_ = new ArrayList<>(universe.getWorlds());
for (World w : worlds_) { for (World w : worlds_) {
worldDocuments.add(w.mongoSerialise()); LogManager.LOGGER.fine("Saving world "+w.getId()+" to mongodb");
insertedWorlds++; insertedWorlds++;
worlds.save(w.mongoSerialise());
if (worldDocuments.size() >= perBatch || insertedWorlds >= GameServer.INSTANCE.getGameUniverse().getWorlds().size()) { // If the world should unload, it is removed from the Universe after having been saved.
worlds.insert(worldDocuments); if (w.shouldUnload()){
worldDocuments.clear(); unloaded_worlds++;
LogManager.LOGGER.fine("Unloading world "+w.getId()+" from universe");
universe.removeWorld(w);
} }
} }
@ -250,25 +258,21 @@ public class GameServer implements Runnable {
insertedUsers++; insertedUsers++;
if (!u.isGuest()) { if (!u.isGuest()) {
userDocuments.add(u.mongoSerialise()); users.save(u.mongoSerialise());
} }
if (userDocuments.size() >= perBatch || insertedUsers >= GameServer.INSTANCE.getGameUniverse().getUsers().size()) {
users.insert(userDocuments);
userDocuments.clear();
}
} }
BasicDBObject serverObj = new BasicDBObject(); BasicDBObject serverObj = new BasicDBObject();
serverObj.put("_id","serverinfo"); // a constant id ensures only one entry is kept and updated, instead of a new entry created every save.
serverObj.put("time", gameUniverse.getTime()); serverObj.put("time", gameUniverse.getTime());
serverObj.put("nextObjectId", gameUniverse.getNextObjectId()); serverObj.put("nextObjectId", gameUniverse.getNextObjectId());
server.insert(serverObj); server.save(serverObj);
LogManager.LOGGER.info(""+insertedWorlds+" worlds saved, "+unloaded_worlds+" unloaded");
LogManager.LOGGER.info("Done!"); LogManager.LOGGER.info("Done!");
} catch (UnknownHostException e) {
e.printStackTrace();
}
} }
public ServerConfiguration getConfig() { public ServerConfiguration getConfig() {

View File

@ -1,5 +1,6 @@
package net.simon987.server.game; package net.simon987.server.game;
import com.mongodb.*;
import net.simon987.server.GameServer; import net.simon987.server.GameServer;
import net.simon987.server.ServerConfiguration; import net.simon987.server.ServerConfiguration;
import net.simon987.server.assembly.Assembler; import net.simon987.server.assembly.Assembler;
@ -9,14 +10,21 @@ import net.simon987.server.assembly.exception.CancelledException;
import net.simon987.server.logging.LogManager; import net.simon987.server.logging.LogManager;
import net.simon987.server.user.User; import net.simon987.server.user.User;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Hashtable;
public class GameUniverse { public class GameUniverse {
private ArrayList<World> worlds; //private ArrayList<World> worlds;
private Hashtable<String,World> worlds;
private ArrayList<User> users; private ArrayList<User> users;
private WorldGenerator worldGenerator; private WorldGenerator worldGenerator;
private MongoClient mongo = null;
private long time; private long time;
private long nextObjectId = 0; private long nextObjectId = 0;
@ -25,63 +33,149 @@ public class GameUniverse {
public GameUniverse(ServerConfiguration config) { public GameUniverse(ServerConfiguration config) {
worlds = new ArrayList<>(32); //worlds = new ArrayList<>(32);
worlds = new Hashtable<String,World>(32);
users = new ArrayList<>(16); users = new ArrayList<>(16);
worldGenerator = new WorldGenerator(config); worldGenerator = new WorldGenerator(config);
}
public void setMongo(MongoClient mongo){
this.mongo = mongo;
} }
public long getTime() { public long getTime() {
return time; return time;
} }
/**
* Attempts loading a world from mongoDB by coordinates
*
* @param x the x coordinate of the world
* @param y the y coordinate of the world
*
* @return World, null if not found
*/
private World loadWorld(int x, int y){
DB db = mongo.getDB("mar");
DBCollection worlds = db.getCollection("world");
BasicDBObject whereQuery = new BasicDBObject();
whereQuery.put("_id", World.idFromCoordinates(x,y));
DBCursor cursor = worlds.find(whereQuery);
if (cursor.hasNext()) {
World w = World.deserialize(cursor.next());
return w;
}
else{
return null;
}
}
/**
* Get a world by coordinates, attempts to load from mongoDB if not found.
*
* @param x the x coordinate of the world
* @param y the y coordinate of the world
* @param createNew if true, a new world is created when a world with given coordinates is not found
*
* @return World, null if not found and not created.
*/
public World getWorld(int x, int y, boolean createNew) { public World getWorld(int x, int y, boolean createNew) {
for (World world : worlds) { // Wrapping coordinates around cyclically
if (world.getX() == x && world.getY() == y) { x %= maxWidth+1;
y %= maxWidth+1;
// Looks for a previously loaded world
World world = getLoadedWorld(x,y);
if (world != null){
return world; return world;
} }
// Tries loading the world from the database
world = loadWorld(x,y);
if (world != null){
addWorld(world);
LogManager.LOGGER.fine("Loaded world "+(World.idFromCoordinates(x,y))+" from mongodb.");
return world;
} }
if (x >= 0 && x <= maxWidth && y >= 0 && y <= maxWidth) { // World does not exist
if (createNew) { if (createNew) {
//World does not exist // Creates a new world
World world = createWorld(x, y); world = createWorld(x, y);
worlds.add(world); addWorld(world);
LogManager.LOGGER.fine("Created new world "+(World.idFromCoordinates(x,y))+".");
return world; return world;
} else { } else {
return null; return null;
} }
}
} else { public World getLoadedWorld(int x, int y) {
return null; // Wrapping coordinates around cyclically
x %= maxWidth+1;
y %= maxWidth+1;
return worlds.get(World.idFromCoordinates(x,y));
}
/**
* Adds a new or freshly loaded world to the universe (if not already present).
*
* @param world the world to be added
*/
public void addWorld(World world){
World w = worlds.get(world.getId());
if (w == null){
world.setUniverse(this);
worlds.put(world.getId(),world);
}
}
/**
* Removes the world with given coordinates from the universe.
*
* @param x the x coordinate of the world to be removed
* @param y the y coordinate of the world to be removed
*/
public void removeWorld(int x, int y){
World w = worlds.remove(World.idFromCoordinates(x,y));
if (w != null){
w.setUniverse(null);
}
}
/**
* Removes the given world from the universe.
*
* @param world the world to be removed.
*/
public void removeWorld(World world){
World w = worlds.remove(world.getId());
if (w != null){
w.setUniverse(null);
} }
} }
public World createWorld(int x, int y) { public World createWorld(int x, int y) {
World world = null; World world = null;
try { try {
world = worldGenerator.generateWorld(x, y); world = worldGenerator.generateWorld(x, y);
} catch (CancelledException e) { } catch (CancelledException e) {
e.printStackTrace(); e.printStackTrace();
} }
return world; return world;
} }
public User getUser(String username) { public User getUser(String username) {
for (User user : users) { for (User user : users) {
if (user.getUsername().equals(username)) { if (user.getUsername().equals(username)) {
return user; return user;
} }
} }
return null; return null;
} }
@ -142,7 +236,7 @@ public class GameUniverse {
public GameObject getObject(long id) { public GameObject getObject(long id) {
// //
for (World world : worlds) { for (World world : getWorlds()) {
for (GameObject object : world.getGameObjects()) { for (GameObject object : world.getGameObjects()) {
if (object.getObjectId() == id) { if (object.getObjectId() == id) {
return object; return object;
@ -160,7 +254,7 @@ public class GameUniverse {
} }
public ArrayList<World> getWorlds() { public ArrayList<World> getWorlds() {
return worlds; return new ArrayList<World>(worlds.values());
} }
public ArrayList<User> getUsers() { public ArrayList<User> getUsers() {

View File

@ -6,9 +6,11 @@ import com.mongodb.DBObject;
import net.simon987.server.GameServer; import net.simon987.server.GameServer;
import net.simon987.server.event.GameEvent; import net.simon987.server.event.GameEvent;
import net.simon987.server.event.WorldUpdateEvent; import net.simon987.server.event.WorldUpdateEvent;
import net.simon987.server.game.GameUniverse;
import net.simon987.server.game.pathfinding.Pathfinder; import net.simon987.server.game.pathfinding.Pathfinder;
import net.simon987.server.io.MongoSerialisable; import net.simon987.server.io.MongoSerialisable;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import net.simon987.server.logging.LogManager;
import java.awt.*; import java.awt.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -57,7 +59,28 @@ public class World implements MongoSerialisable {
public boolean isTileBlocked(int x, int y) { public boolean isTileBlocked(int x, int y) {
return getGameObjectsBlockingAt(x, y).size() > 0 || tileMap.getTileAt(x, y) == TileMap.WALL_TILE; return getGameObjectsBlockingAt(x, y).size() > 0 || tileMap.getTileAt(x, y) == TileMap.WALL_TILE;
}
/**
* Computes the world's unique id from its coordinates.
*
* @param x the x coordinate of the world
* @param y the y coordinate of the world
*
* @return long
*/
public static String idFromCoordinates(int x, int y){
return "w-"+"0x"+Integer.toHexString(x)+"-"+"0x"+Integer.toHexString(y);
//return ((long)x)*(((long)maxWidth)+1)+((long)y);
}
/**
* Returns the world's unique id, computed with idFromCoordinates.
*
* @return long
*/
public String getId(){
return World.idFromCoordinates(x,y);
} }
public int getX() { public int getX() {
@ -124,6 +147,9 @@ public class World implements MongoSerialisable {
objects.add(obj.mongoSerialise()); objects.add(obj.mongoSerialise());
} }
dbObject.put("_id", getId());
dbObject.put("objects", objects); dbObject.put("objects", objects);
dbObject.put("terrain", tileMap.mongoSerialise()); dbObject.put("terrain", tileMap.mongoSerialise());
@ -131,7 +157,7 @@ public class World implements MongoSerialisable {
dbObject.put("y", y); dbObject.put("y", y);
dbObject.put("updatable", updatable); dbObject.put("updatable", updatable);
dbObject.put("shouldUpdate",shouldUpdate());
return dbObject; return dbObject;
} }
@ -330,4 +356,73 @@ public class World implements MongoSerialisable {
public boolean shouldUpdate() { public boolean shouldUpdate() {
return updatable > 0; return updatable > 0;
} }
private GameUniverse universe = null;
public void setUniverse(GameUniverse universe){
this.universe = universe;
}
public ArrayList<World> getNeighbouringLoadedWorlds(){
ArrayList<World> neighbouringWorlds = new ArrayList<>();
if (universe == null){
return neighbouringWorlds;
}
for (int dx=-1; dx<=+1; dx+=2){
World nw = universe.getLoadedWorld(x+dx,y);
if (nw != null){
neighbouringWorlds.add(nw);
}
}
for (int dy=-1; dy<=+1; dy+=2){
World nw = universe.getLoadedWorld(x,y+dy);
if (nw != null){
neighbouringWorlds.add(nw);
}
}
return neighbouringWorlds;
}
public ArrayList<World> getNeighbouringExistingWorlds(){
ArrayList<World> neighbouringWorlds = new ArrayList<>();
if (universe == null){
return neighbouringWorlds;
}
for (int dx=-1; dx<=+1; dx+=2){
World nw = universe.getWorld(x+dx,y,false);
if (nw != null){
neighbouringWorlds.add(nw);
}
}
for (int dy=-1; dy<=+1; dy+=2){
World nw = universe.getWorld(x,y+dy,false);
if (nw != null){
neighbouringWorlds.add(nw);
}
}
return neighbouringWorlds;
}
public boolean canUnload(){
return updatable==0;
}
public boolean shouldUnload(){
boolean res = canUnload();
for (World nw : getNeighbouringLoadedWorlds() ){
res &= nw.canUnload();
}
return res;
}
} }

View File

@ -44,6 +44,7 @@ public class User implements MongoSerialisable {
BasicDBObject dbObject = new BasicDBObject(); BasicDBObject dbObject = new BasicDBObject();
dbObject.put("_id", username); // a constant id ensures only one entry per user is kept and updated, instead of a new entry created every save for every user.
dbObject.put("username", username); dbObject.put("username", username);
dbObject.put("code", userCode); dbObject.put("code", userCode);
dbObject.put("controlledUnit", controlledUnit.getObjectId()); dbObject.put("controlledUnit", controlledUnit.getObjectId());