Browse Source
The biggest part of this commit is a plugin storage subsystem, which at this point I'm fairly happy with. I've iterated on this a couple times, and the final result has a very clean/simple interface, is easy to extend to different data stores, and has a very few minimal number of grokable edge cases. - Storage subsytem - Fix command group abbreviations - Fix reconnecting in the GatewaySocket - Add pickle support to serializerpull/5/head
17 changed files with 300 additions and 89 deletions
@ -1,8 +0,0 @@ |
|||||
from .memory import MemoryBackend |
|
||||
from .disk import DiskBackend |
|
||||
|
|
||||
|
|
||||
BACKENDS = { |
|
||||
'memory': MemoryBackend, |
|
||||
'disk': DiskBackend, |
|
||||
} |
|
@ -1,20 +0,0 @@ |
|||||
|
|
||||
class BaseStorageBackend(object): |
|
||||
def base(self): |
|
||||
return self.storage |
|
||||
|
|
||||
def __getitem__(self, key): |
|
||||
return self.storage[key] |
|
||||
|
|
||||
def __setitem__(self, key, value): |
|
||||
self.storage[key] = value |
|
||||
|
|
||||
def __delitem__(self, key): |
|
||||
del self.storage[key] |
|
||||
|
|
||||
|
|
||||
class StorageDict(dict): |
|
||||
def ensure(self, name): |
|
||||
if not dict.__contains__(self, name): |
|
||||
dict.__setitem__(self, name, StorageDict()) |
|
||||
return dict.__getitem__(self, name) |
|
@ -1,35 +0,0 @@ |
|||||
import os |
|
||||
|
|
||||
from .base import BaseStorageBackend, StorageDict |
|
||||
|
|
||||
|
|
||||
class DiskBackend(BaseStorageBackend): |
|
||||
def __init__(self, config): |
|
||||
self.format = config.get('format', 'json') |
|
||||
self.path = config.get('path', 'storage') + '.' + self.format |
|
||||
self.storage = StorageDict() |
|
||||
|
|
||||
@staticmethod |
|
||||
def get_format_functions(fmt): |
|
||||
if fmt == 'json': |
|
||||
from json import loads, dumps |
|
||||
return (loads, dumps) |
|
||||
elif fmt == 'yaml': |
|
||||
from pyyaml import load, dump |
|
||||
return (load, dump) |
|
||||
raise Exception('Unsupported format type {}'.format(fmt)) |
|
||||
|
|
||||
def load(self): |
|
||||
if not os.path.exists(self.path): |
|
||||
return |
|
||||
|
|
||||
decode, _ = self.get_format_functions(self.format) |
|
||||
|
|
||||
with open(self.path, 'r') as f: |
|
||||
self.storage = decode(f.read()) |
|
||||
|
|
||||
def dump(self): |
|
||||
_, encode = self.get_format_functions(self.format) |
|
||||
|
|
||||
with open(self.path, 'w') as f: |
|
||||
f.write(encode(self.storage)) |
|
@ -1,7 +0,0 @@ |
|||||
from .base import BaseStorageBackend, StorageDict |
|
||||
|
|
||||
|
|
||||
class MemoryBackend(BaseStorageBackend): |
|
||||
def __init__(self, config): |
|
||||
self.storage = StorageDict() |
|
||||
|
|
@ -0,0 +1,15 @@ |
|||||
|
import inspect |
||||
|
import importlib |
||||
|
|
||||
|
from .base import BaseProvider |
||||
|
|
||||
|
|
||||
|
def load_provider(name): |
||||
|
try: |
||||
|
mod = importlib.import_module('disco.bot.providers.' + name) |
||||
|
except ImportError: |
||||
|
mod = importlib.import_module(name) |
||||
|
|
||||
|
for entry in filter(inspect.isclass, map(lambda i: getattr(mod, i), dir(mod))): |
||||
|
if issubclass(entry, BaseProvider) and entry != BaseProvider: |
||||
|
return entry |
@ -0,0 +1,136 @@ |
|||||
|
import six |
||||
|
import pickle |
||||
|
|
||||
|
from six.moves import map |
||||
|
|
||||
|
from UserDict import UserDict |
||||
|
|
||||
|
|
||||
|
ROOT_SENTINEL = u'\u200B' |
||||
|
SEP_SENTINEL = u'\u200D' |
||||
|
OBJ_SENTINEL = u'\u200C' |
||||
|
CAST_SENTINEL = u'\u24EA' |
||||
|
|
||||
|
|
||||
|
def join_key(*args): |
||||
|
nargs = [] |
||||
|
for arg in args: |
||||
|
if not isinstance(arg, six.string_types): |
||||
|
arg = CAST_SENTINEL + pickle.dumps(arg) |
||||
|
nargs.append(arg) |
||||
|
return SEP_SENTINEL.join(nargs) |
||||
|
|
||||
|
|
||||
|
def true_key(key): |
||||
|
key = key.rsplit(SEP_SENTINEL, 1)[-1] |
||||
|
if key.startswith(CAST_SENTINEL): |
||||
|
return pickle.loads(key) |
||||
|
return key |
||||
|
|
||||
|
|
||||
|
class BaseProvider(object): |
||||
|
def __init__(self, config): |
||||
|
self.config = config |
||||
|
self.data = {} |
||||
|
|
||||
|
def exists(self, key): |
||||
|
return key in self.data |
||||
|
|
||||
|
def keys(self, other): |
||||
|
count = other.count(SEP_SENTINEL) + 1 |
||||
|
for key in self.data.keys(): |
||||
|
if key.startswith(other) and key.count(SEP_SENTINEL) == count: |
||||
|
yield key |
||||
|
|
||||
|
def get_many(self, keys): |
||||
|
for key in keys: |
||||
|
yield key, self.get(key) |
||||
|
|
||||
|
def get(self, key): |
||||
|
return self.data[key] |
||||
|
|
||||
|
def set(self, key, value): |
||||
|
self.data[key] = value |
||||
|
|
||||
|
def delete(self, key): |
||||
|
del self.data[key] |
||||
|
|
||||
|
def load(self): |
||||
|
pass |
||||
|
|
||||
|
def save(self): |
||||
|
pass |
||||
|
|
||||
|
def root(self): |
||||
|
return StorageDict(self) |
||||
|
|
||||
|
|
||||
|
class StorageDict(UserDict): |
||||
|
def __init__(self, parent_or_provider, key=None): |
||||
|
if isinstance(parent_or_provider, BaseProvider): |
||||
|
self.provider = parent_or_provider |
||||
|
self.parent = None |
||||
|
else: |
||||
|
self.parent = parent_or_provider |
||||
|
self.provider = self.parent.provider |
||||
|
self._key = key or ROOT_SENTINEL |
||||
|
|
||||
|
def keys(self): |
||||
|
return map(true_key, self.provider.keys(self.key)) |
||||
|
|
||||
|
def values(self): |
||||
|
for key in self.keys(): |
||||
|
yield self.provider.get(key) |
||||
|
|
||||
|
def items(self): |
||||
|
for key in self.keys(): |
||||
|
yield (true_key(key), self.provider.get(key)) |
||||
|
|
||||
|
def ensure(self, key, typ=dict): |
||||
|
if key not in self: |
||||
|
self[key] = typ() |
||||
|
return self[key] |
||||
|
|
||||
|
def update(self, obj): |
||||
|
for k, v in six.iteritems(obj): |
||||
|
self[k] = v |
||||
|
|
||||
|
@property |
||||
|
def data(self): |
||||
|
obj = {} |
||||
|
|
||||
|
for raw, value in self.provider.get_many(self.provider.keys(self.key)): |
||||
|
key = true_key(raw) |
||||
|
|
||||
|
if value == OBJ_SENTINEL: |
||||
|
value = self.__class__(self, key=key).data |
||||
|
obj[key] = value |
||||
|
return obj |
||||
|
|
||||
|
@property |
||||
|
def key(self): |
||||
|
if self.parent is not None: |
||||
|
return join_key(self.parent.key, self._key) |
||||
|
return self._key |
||||
|
|
||||
|
def __setitem__(self, key, value): |
||||
|
if isinstance(value, dict): |
||||
|
obj = self.__class__(self, key) |
||||
|
obj.update(value) |
||||
|
value = OBJ_SENTINEL |
||||
|
|
||||
|
self.provider.set(join_key(self.key, key), value) |
||||
|
|
||||
|
def __getitem__(self, key): |
||||
|
res = self.provider.get(join_key(self.key, key)) |
||||
|
|
||||
|
if res == OBJ_SENTINEL: |
||||
|
return self.__class__(self, key) |
||||
|
|
||||
|
return res |
||||
|
|
||||
|
def __delitem__(self, key): |
||||
|
return self.provider.delete(join_key(self.key, key)) |
||||
|
|
||||
|
def __contains__(self, key): |
||||
|
return self.provider.exists(join_key(self.key, key)) |
@ -0,0 +1,53 @@ |
|||||
|
import os |
||||
|
import gevent |
||||
|
|
||||
|
from disco.util.serializer import Serializer |
||||
|
from .base import BaseProvider |
||||
|
|
||||
|
|
||||
|
class DiskProvider(BaseProvider): |
||||
|
def __init__(self, config): |
||||
|
super(DiskProvider, self).__init__(config) |
||||
|
self.format = config.get('format', 'pickle') |
||||
|
self.path = config.get('path', 'storage') + '.' + self.format |
||||
|
self.fsync = config.get('fsync', False) |
||||
|
self.fsync_changes = config.get('fsync_changes', 1) |
||||
|
|
||||
|
self.change_count = 0 |
||||
|
|
||||
|
def autosave_loop(self, interval): |
||||
|
while True: |
||||
|
gevent.sleep(interval) |
||||
|
self.save() |
||||
|
|
||||
|
def _on_change(self): |
||||
|
if self.fsync: |
||||
|
self.change_count += 1 |
||||
|
|
||||
|
if self.change_count >= self.fsync_changes: |
||||
|
self.save() |
||||
|
self.change_count = 0 |
||||
|
|
||||
|
def load(self): |
||||
|
if not os.path.exists(self.path): |
||||
|
return |
||||
|
|
||||
|
if self.config.get('autosave', True): |
||||
|
self.autosave_task = gevent.spawn( |
||||
|
self.autosave_loop, |
||||
|
self.config.get('autosave_interval', 120)) |
||||
|
|
||||
|
with open(self.path, 'r') as f: |
||||
|
self.data = Serializer.loads(self.format, f.read()) |
||||
|
|
||||
|
def save(self): |
||||
|
with open(self.path, 'w') as f: |
||||
|
f.write(Serializer.dumps(self.format, self.data)) |
||||
|
|
||||
|
def set(self, key, value): |
||||
|
super(DiskProvider, self).set(key, value) |
||||
|
self._on_change() |
||||
|
|
||||
|
def delete(self, key): |
||||
|
super(DiskProvider, self).delete(key) |
||||
|
self._on_change() |
@ -0,0 +1,5 @@ |
|||||
|
from .base import BaseProvider |
||||
|
|
||||
|
|
||||
|
class MemoryProvider(BaseProvider): |
||||
|
pass |
@ -0,0 +1,50 @@ |
|||||
|
from __future__ import absolute_import |
||||
|
|
||||
|
import six |
||||
|
import rocksdb |
||||
|
|
||||
|
from itertools import izip |
||||
|
from six.moves import map |
||||
|
|
||||
|
from disco.util.serializer import Serializer |
||||
|
from .base import BaseProvider, SEP_SENTINEL |
||||
|
|
||||
|
|
||||
|
class RocksDBProvider(BaseProvider): |
||||
|
def __init__(self, config): |
||||
|
self.config = config |
||||
|
self.format = config.get('format', 'pickle') |
||||
|
self.path = config.get('path', 'storage.db') |
||||
|
|
||||
|
def k(self, k): |
||||
|
return bytes(k) if six.PY3 else str(k.encode('utf-8')) |
||||
|
|
||||
|
def load(self): |
||||
|
self.db = rocksdb.DB(self.path, rocksdb.Options(create_if_missing=True)) |
||||
|
|
||||
|
def exists(self, key): |
||||
|
return self.db.get(self.k(key)) is not None |
||||
|
|
||||
|
# TODO prefix extractor |
||||
|
def keys(self, other): |
||||
|
count = other.count(SEP_SENTINEL) + 1 |
||||
|
it = self.db.iterkeys() |
||||
|
it.seek_to_first() |
||||
|
|
||||
|
for key in it: |
||||
|
key = key.decode('utf-8') |
||||
|
if key.startswith(other) and key.count(SEP_SENTINEL) == count: |
||||
|
yield key |
||||
|
|
||||
|
def get_many(self, keys): |
||||
|
for key, value in izip(keys, self.db.multi_get(list(map(self.k, keys)))): |
||||
|
yield (key, Serializer.loads(self.format, value.decode('utf-8'))) |
||||
|
|
||||
|
def get(self, key): |
||||
|
return Serializer.loads(self.format, self.db.get(self.k(key)).decode('utf-8')) |
||||
|
|
||||
|
def set(self, key, value): |
||||
|
self.db.put(self.k(key), Serializer.dumps(self.format, value)) |
||||
|
|
||||
|
def delete(self, key): |
||||
|
self.db.delete(self.k(key)) |
@ -1,21 +1,26 @@ |
|||||
from .backends import BACKENDS |
from .providers import load_provider |
||||
|
|
||||
|
|
||||
class Storage(object): |
class Storage(object): |
||||
def __init__(self, ctx, config): |
def __init__(self, ctx, config): |
||||
self.ctx = ctx |
self.ctx = ctx |
||||
self.backend = BACKENDS[config.backend] |
self.config = config |
||||
# TODO: autosave |
self.provider = load_provider(config.provider)(config.config) |
||||
# config.autosave config.autosave_interval |
self.provider.load() |
||||
|
self.root = self.provider.root() |
||||
|
|
||||
|
@property |
||||
|
def plugin(self): |
||||
|
return self.root.ensure('plugins').ensure(self.ctx['plugin'].name) |
||||
|
|
||||
@property |
@property |
||||
def guild(self): |
def guild(self): |
||||
return self.backend.base().ensure('guilds').ensure(self.ctx['guild'].id) |
return self.plugin.ensure('guilds').ensure(self.ctx['guild'].id) |
||||
|
|
||||
@property |
@property |
||||
def channel(self): |
def channel(self): |
||||
return self.backend.base().ensure('channels').ensure(self.ctx['channel'].id) |
return self.plugin.ensure('channels').ensure(self.ctx['channel'].id) |
||||
|
|
||||
@property |
@property |
||||
def user(self): |
def user(self): |
||||
return self.backend.base().ensure('users').ensure(self.ctx['user'].id) |
return self.plugin.ensure('users').ensure(self.ctx['user'].id) |
||||
|
Loading…
Reference in new issue