Save chunks in db

One of the changes introduced for this is that multiple chunks and
diffs are now represented by dicts, not lists.
This commit is contained in:
Joscha 2017-04-16 15:51:00 +00:00
parent 9e5b5f874a
commit aca387cf05
4 changed files with 121 additions and 70 deletions

View file

@ -25,13 +25,21 @@ class ChunkDiff():
diff = cls() diff = cls()
diff._chars = {int(i): v for i, v in d.items()} diff._chars = {int(i): v for i, v in d.items()}
return diff return diff
#self._chars = d.copy()
def to_dict(self):
return self._chars
#return self._chars.copy()
@classmethod @classmethod
def from_string(cls, s): def from_string(cls, s):
diff = cls() diff = cls()
#for c in string for i, char in enumerate(s):
pass diff._chars[i] = char
return diff
def to_string(self):
s = "".join(self._chars.get(i, " ") for i in range(CHUNK_WIDTH*CHUNK_HEIGHT))
return s
def copy(self): def copy(self):
return ChunkDiff.from_dict(self.to_dict().copy()) return ChunkDiff.from_dict(self.to_dict().copy())
@ -41,10 +49,6 @@ class ChunkDiff():
newdiff.apply(diff) newdiff.apply(diff)
return newdiff return newdiff
def to_dict(self):
return self._chars
#return self._chars.copy()
def set(self, x, y, character): def set(self, x, y, character):
pos = x+y*CHUNK_WIDTH pos = x+y*CHUNK_WIDTH
self._chars[pos] = character self._chars[pos] = character
@ -60,8 +64,7 @@ class ChunkDiff():
self._chars[i] = c self._chars[i] = c
def lines(self): def lines(self):
d = self._chars s = self.to_string()
s = "".join(d.get(i, " ") for i in range(CHUNK_WIDTH*CHUNK_HEIGHT))
return [s[i:i+CHUNK_WIDTH] for i in range(0, CHUNK_WIDTH*CHUNK_HEIGHT, CHUNK_WIDTH)] return [s[i:i+CHUNK_WIDTH] for i in range(0, CHUNK_WIDTH*CHUNK_HEIGHT, CHUNK_WIDTH)]
def empty(self): def empty(self):
@ -83,19 +86,18 @@ class ChunkDiff():
def jsonify_diffs(diffs): def jsonify_diffs(diffs):
ddiffs = [] ddiffs = []
for dchunk in diffs: for pos, diff in diffs.items():
pos = dchunk[0] ddiff = diff.to_dict()
ddiff = dchunk[1].to_dict()
ddiffs.append((pos, ddiff)) ddiffs.append((pos, ddiff))
return ddiffs return ddiffs
def dejsonify_diffs(ddiffs): def dejsonify_diffs(ddiffs):
diffs = [] diffs = {}
for dchunk in ddiffs: for dchunk in ddiffs:
pos = Position(dchunk[0][0], dchunk[0][1]) pos = Position(dchunk[0][0], dchunk[0][1])
diff = ChunkDiff.from_dict(dchunk[1]) diff = ChunkDiff.from_dict(dchunk[1])
diffs.append((pos, diff)) diffs[pos] = diff
return diffs return diffs
@ -114,6 +116,15 @@ class Chunk():
self.last_modified = 0 self.last_modified = 0
@classmethod
def from_string(cls, s):
chunk = cls()
chunk._content = ChunkDiff.from_string(s)
return chunk
def to_string(self):
return self.as_diff().to_string()
def set(self, x, y, character): def set(self, x, y, character):
self._modifications.set(x, y, character) self._modifications.set(x, y, character)
self.touch() self.touch()
@ -191,9 +202,7 @@ class ChunkPool():
return chunk return chunk
def apply_diffs(self, diffs): def apply_diffs(self, diffs):
for dchunk in diffs: for pos, diff in diffs.items():
pos = dchunk[0]
diff = dchunk[1]
chunk = self.get(pos) or self.create(pos) chunk = self.get(pos) or self.create(pos)
#chunk = self.load(pos) #chunk = self.load(pos)
@ -201,9 +210,7 @@ class ChunkPool():
chunk.apply_diff(diff) chunk.apply_diff(diff)
def commit_diffs(self, diffs): def commit_diffs(self, diffs):
for dchunk in diffs: for pos, diff in diffs.items():
pos = dchunk[0]
diff = dchunk[1]
chunk = self.get(pos) or self.create(pos) chunk = self.get(pos) or self.create(pos)
#chunk = self.load(pos) #chunk = self.load(pos)
@ -211,11 +218,11 @@ class ChunkPool():
chunk.commit_diff(diff) chunk.commit_diff(diff)
def commit_changes(self): def commit_changes(self):
changes = [] changes = {}
for pos, chunk in self._chunks.items(): for pos, chunk in self._chunks.items():
if chunk.modified(): if chunk.modified():
changes.append((pos, chunk.get_changes())) changes[pos] = chunk.get_changes()
chunk.commit_changes() chunk.commit_changes()
return changes return changes

View file

@ -31,7 +31,7 @@ class ClientChunkPool(ChunkPool):
def save_changes(self): def save_changes(self):
diffs = self.commit_changes() diffs = self.commit_changes()
# filter out empty changes/chunks # filter out empty changes/chunks
diffs = [dchunk for dchunk in diffs if not dchunk[1].empty()] diffs = {pos: diff for pos, diff in diffs.items() if not diff.empty()}
if diffs: if diffs:
self._client.send_changes(diffs) self._client.send_changes(diffs)

View file

@ -2,8 +2,8 @@ import sqlite3
import time import time
import threading import threading
from chunks import ChunkPool from chunks import ChunkPool, Chunk
from utils import Position from utils import Position, CHUNK_WIDTH, CHUNK_HEIGHT
class ChunkDB(): class ChunkDB():
""" """
@ -13,6 +13,8 @@ class ChunkDB():
def __init__(self, filename): def __init__(self, filename):
self.dbfilename = filename self.dbfilename = filename
self._create_table()
def transaction(func): def transaction(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
con = sqlite3.connect(self.dbfilename) con = sqlite3.connect(self.dbfilename)
@ -24,14 +26,56 @@ class ChunkDB():
return wrapper return wrapper
@transaction
def _create_table(self, con):
cur = con.cursor()
cur.execute(("CREATE TABLE IF NOT EXISTS chunks ("
"x INTEGER NOT NULL, "
"y INTEGER NOT NULL, "
"content TEXT, "
"PRIMARY KEY (x, y)"
")"))
@transaction @transaction
def save_many(self, con, chunks): def save_many(self, con, chunks):
print("save_many") lchunks = ChunkDB.chunks_to_list(chunks)
con.executemany("INSERT OR REPLACE INTO chunks VALUES (?, ?, ?)", lchunks)
@transaction @transaction
def load_many(self, con, coords): def load_many(self, con, coords):
print("load_many") cur = con.cursor()
return [(coor, None) for coor in coords] results = []
for pos in coords:
cur.execute("SELECT * FROM chunks WHERE x=? AND y=?", pos)
results.extend(cur.fetchall())
results = ChunkDB.list_to_chunks(results)
return results
@transaction
def remove_empty(self, con):
con.execute("DELETE FROM chunks WHERE content=?", (" "*CHUNK_WIDTH*CHUNK_HEIGHT,))
@staticmethod
def list_to_chunks(l):
chunks = {}
for item in l:
pos = Position(item[0], item[1])
chunk = Chunk.from_string(item[2])
chunks[pos] = chunk
return chunks
@staticmethod
def chunks_to_list(chunks):
l = []
for pos, chunk in chunks.items():
l.append((pos[0], pos[1], chunk.to_string()))
return l
class DBChunkPool(ChunkPool): class DBChunkPool(ChunkPool):
""" """
@ -42,8 +86,8 @@ class DBChunkPool(ChunkPool):
super().__init__() super().__init__()
self._chunkdb = ChunkDB(filename) self._chunkdb = ChunkDB(filename)
self.save_period = 10 # save and clean up every minute self.save_period = 60 # save and clean up every minute
self.max_age = 20 # ca. one minute until a chunk is unloaded again self.max_age = 60 # ca. one minute until a chunk is unloaded again
self.save_thread = threading.Thread( self.save_thread = threading.Thread(
target=self.perodic_save, target=self.perodic_save,
@ -55,27 +99,23 @@ class DBChunkPool(ChunkPool):
def save_changes(self): def save_changes(self):
diffs = self.commit_changes() diffs = self.commit_changes()
changed_chunks = [] changed_chunks = {}
for dchunk in diffs: for pos, diff in diffs.items():
pos = dchunk[0]
chunk = self.get(pos) chunk = self.get(pos)
changed_chunks.append((pos, chunk)) changed_chunks[pos] = chunk
self._chunkdb.save_many(changed_chunks) self._chunkdb.save_many(changed_chunks)
def load(self, pos): def load(self, pos):
print("Loading individual chunk...")
raise Exception raise Exception
def load_list(self, coords): def load_list(self, coords):
print("Loading chunk list...")
to_load = [pos for pos in coords if pos not in self._chunks] to_load = [pos for pos in coords if pos not in self._chunks]
chunks = self._chunkdb.load_many(to_load) chunks = self._chunkdb.load_many(to_load)
for dchunk in chunks:
pos = dchunk[0] for pos in to_load:
chunk = dchunk[1] if pos in chunks:
if chunk: self.set(pos, chunks.get(pos))
self.set(pos, chunk)
else: else:
self.create(pos) self.create(pos)
@ -84,21 +124,20 @@ class DBChunkPool(ChunkPool):
time.sleep(self.save_period) time.sleep(self.save_period)
with self: with self:
print("BEFORE:::")
self.print_chunks()
self.save_changes() self.save_changes()
# unload old chunks # unload old chunks
now = time.time() now = time.time()
for pos, chunk in self._chunks.items():
print(f"p{pos} :: t{now} :: m{chunk.last_modified} :: a{chunk.age(now)}")
self.clean_up(condition=lambda pos, chunk: chunk.age(now) > self.max_age) self.clean_up(condition=lambda pos, chunk: chunk.age(now) > self.max_age)
print("AFTER:::") def remove_empty(self):
self.print_chunks() self._chunkdb.remove_empty()
def _get_min_max(self):
"""
Meant for debugging.
"""
def get_min_max(self):
minx = min(pos.x for pos in self._chunks) minx = min(pos.x for pos in self._chunks)
maxx = max(pos.x for pos in self._chunks) maxx = max(pos.x for pos in self._chunks)
miny = min(pos.y for pos in self._chunks) miny = min(pos.y for pos in self._chunks)
@ -106,9 +145,13 @@ class DBChunkPool(ChunkPool):
return minx, maxx, miny, maxy return minx, maxx, miny, maxy
def print_chunks(self): def _print_chunks(self):
"""
Meant for debugging.
"""
if self._chunks: if self._chunks:
minx, maxx, miny, maxy = self.get_min_max() minx, maxx, miny, maxy = self._get_min_max()
sizex, sizey = maxx - minx + 1, maxy - miny + 1 sizex, sizey = maxx - minx + 1, maxy - miny + 1
print("" + ""*sizex*2 + "") print("" + ""*sizex*2 + "")
for y in range(miny, maxy + 1): for y in range(miny, maxy + 1):

View file

@ -12,14 +12,16 @@ from chunks import ChunkPool
class WotServer(WebSocket): class WotServer(WebSocket):
def handle_request_chunks(self, coords): def handle_request_chunks(self, coords):
diffs = [] diffs = {}
coords = [Position(coor[0], coor[1]) for coor in coords]
with self.pool as pool: with self.pool as pool:
coords = [Position(coor[0], coor[1]) for coor in coords]
pool.load_list(coords) pool.load_list(coords)
for pos in coords: for pos in coords:
chunk = pool.get(pos) chunk = pool.get(pos)
diffs.append((pos, chunk.as_diff())) diff = chunk.as_diff()
diffs[pos] = diff
self.loaded_chunks.add(pos) self.loaded_chunks.add(pos)
@ -38,16 +40,17 @@ class WotServer(WebSocket):
# check whether changes are correct (exclude certain characters) # check whether changes are correct (exclude certain characters)
# if not correct, send corrections them back to sender # if not correct, send corrections them back to sender
legitimate_diffs = [] legitimate_diffs = {}
illegitimate_diffs = [] illegitimate_diffs = {}
for dchunk in diffs: for pos, diff in diffs.items():
if dchunk[1].legitimate(): if diff.legitimate():
legitimate_diffs.append(dchunk) legitimate_diffs[pos] = diff
else: else:
illegitimate_diffs.append(dchunk) illegitimate_diffs[pos] = diff
if legitimate_diffs: if legitimate_diffs:
with self.pool as pool: with self.pool as pool:
pool.load_list(legitimate_diffs.keys())
pool.apply_diffs(legitimate_diffs) pool.apply_diffs(legitimate_diffs)
for client in self.clients: for client in self.clients:
@ -61,23 +64,19 @@ class WotServer(WebSocket):
self.sendMessage(json.dumps(message)) self.sendMessage(json.dumps(message))
def reverse_diffs(self, diffs): def reverse_diffs(self, diffs):
coords = [dchunk[0] for dchunk in diffs]
with self.pool as pool: with self.pool as pool:
pool.load_list(coords) pool.load_list(diffs.keys())
reverse_diffs = [] reverse_diffs = {}
for dchunk in diffs: for pos, diff in diffs.items():
pos = dchunk[0]
diff = dchunk[1]
chunk = pool.get(pos) chunk = pool.get(pos)
reverse_diff = diff.diff(chunk.as_diff()) reverse_diff = diff.diff(chunk.as_diff())
reverse_diffs.append((pos, reverse_diff)) reverse_diffs[pos] = reverse_diff
return reverse_diffs return reverse_diffs
def send_changes(self, diffs): def send_changes(self, diffs):
diffs = [dchunk for dchunk in diffs if dchunk[0] in self.loaded_chunks] diffs = {pos: diff for pos, diff in diffs.items() if pos in self.loaded_chunks}
ddiffs = jsonify_diffs(diffs) ddiffs = jsonify_diffs(diffs)
if ddiffs: if ddiffs:
@ -146,6 +145,8 @@ def main(argv):
print("") print("")
print("Saving recent changes.") print("Saving recent changes.")
WotServer.pool.save_changes() WotServer.pool.save_changes()
print("Cleaning up empty chunks from db.")
WotServer.pool.remove_empty()
print("Stopped.") print("Stopped.")
if __name__ == "__main__": if __name__ == "__main__":