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): |
|||
def __init__(self, ctx, config): |
|||
self.ctx = ctx |
|||
self.backend = BACKENDS[config.backend] |
|||
# TODO: autosave |
|||
# config.autosave config.autosave_interval |
|||
self.config = config |
|||
self.provider = load_provider(config.provider)(config.config) |
|||
self.provider.load() |
|||
self.root = self.provider.root() |
|||
|
|||
@property |
|||
def plugin(self): |
|||
return self.root.ensure('plugins').ensure(self.ctx['plugin'].name) |
|||
|
|||
@property |
|||
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 |
|||
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 |
|||
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