anki-sync-server/AnkiServer/collection.py

209 lines
6.4 KiB
Python

import anki
import anki.storage
from threading import Thread
from Queue import Queue
try:
import simplejson as json
except ImportError:
import json
import os, errno, time, logging
__all__ = ['CollectionThread']
# TODO: I feel like we shouldn't need this wrapper...
class CollectionWrapper(object):
"""A simple wrapper around a collection for the purpose of opening and closing on demand
as well as doing special initialization."""
def __init__(self, path):
self.path = os.path.realpath(path)
self._col = None
def _create_colection(self):
# mkdir -p the path, because it might not exist
dirname = os.path.dirname(self.path)
try:
os.makedirs(dirname)
except OSError, exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
col = anki.Storage.Collection(self.path)
# Do any special setup
self.setup_new_collection(col)
return col
def setup_new_collection(self, col):
"""Override this function to initial collections in some special way."""
pass
def open(self):
if self._col is None:
if os.path.exists(self.path):
self._col = anki.DeckStorage.Deck(self.path)
else:
self._deck = self._create_deck()
return self._deck
def close(self):
if self._col is None:
return
self._col.close()
self._col = None
def opened(self):
return self._col is not None
class CollectionThread(object):
def __init__(self, path, wrapper_class=CollectionWrapper):
self.path = os.path.realpath(path)
self.wrapper = wrapper_class(path)
self._queue = Queue()
self._thread = None
self._running = False
self.last_timestamp = time.time()
@property
def running(self):
return self._running
def qempty(self):
return self._queue.empty()
def current(self):
from threading import current_thread
return current_thread() == self._thread
def execute(self, func, args=[], kw={}, waitForReturn=True):
""" Executes a given function on this thread with the *args and **kw.
If 'waitForReturn' is True, then it will block until the function has
executed and return its return value. If False, it will return None
immediately and the function will be executed sometime later.
"""
if waitForReturn:
return_queue = Queue()
else:
return_queue = None
self._queue.put((func, args, kw, return_queue))
if return_queue is not None:
ret = return_queue.get(True)
if isinstance(ret, Exception):
raise ret
return ret
def _run(self):
logging.info('DeckThread[%s]: Starting...', self.path)
try:
while self._running:
func, args, kw, return_queue = self._queue.get(True)
logging.info('DeckThread[%s]: Running %s(*%s, **%s)', self.path, func.func_name, repr(args), repr(kw))
self.last_timestamp = time.time()
try:
ret = func(*args, **kw)
except Exception, e:
logging.error('DeckThread[%s]: Unable to %s(*%s, **%s): %s',
self.path, func.func_name, repr(args), repr(kw), e, exc_info=True)
# we return the Exception which will be raise'd on the other end
ret = e
if return_queue is not None:
return_queue.put(ret)
except Exception, e:
logging.error('DeckThread[%s]: Thread crashed! Exception: %s', e, exc_info=True)
finally:
self.wrapper.close()
# clean out old thread object
self._thread = None
# in case we got here via an exception
self._running = False
logging.info('DeckThread[%s]: Stopped!' % self.path)
def start(self):
if not self._running:
self._running = True
assert self._thread is None
self._thread = Thread(target=self._run)
self._thread.start()
def stop(self):
def _stop():
self._running = False
self.execute(_stop, waitForReturn=False)
def stop_and_wait(self):
""" Tell the thread to stop and wait for it to happen. """
self.stop()
if self._thread is not None:
self._thread.join()
class CollectionThreadPool(object):
def __init__(self, wrapper_class=CollectionWrapper):
self.wrapper_class = wrapper_class
self.threads = {}
self.monitor_frequency = 15
self.monitor_inactivity = 90
monitor = Thread(target=self._monitor_run)
monitor.daemon = True
monitor.start()
self._monitor_thread = monitor
# TODO: it would be awesome to have a safe way to stop inactive threads completely!
# TODO: we need a way to inform other code that the collection has been closed
def _monitor_run(self):
""" Monitors threads for inactivity and closes the collection on them
(leaves the thread itself running -- hopefully waiting peacefully with only a
small memory footprint!) """
while True:
cur = time.time()
for path, thread in self.threads.items():
if thread.running and thread.wrapper.opened() and thread.qempty() and cur - thread.last_timestamp >= self.monitor_inactivity:
logging.info('Monitor is closing collection on inactive CollectionThread[%s]' % thread.path)
def closeCollection(wrapper):
wrapper.close()
thread.execute(closeCollection, [thread.wrapper], waitForReturn=False)
time.sleep(self.monitor_frequency)
def create_thread(self, path):
return CollectionThread(path, wrapper_class=self.wrapper_class)
def start(self, path):
path = os.path.realpath(path)
try:
thread = self.threads[path]
except KeyError:
thread = self.threads[path] = self.create_thread(path)
thread.start()
return thread
def shutdown(self):
for thread in self.threads.values():
thread.stop()
self.threads = {}
# TODO: There's got to be a way to do this without initializing it ALWAYS!
thread_pool = CollectionThreadPool()