Source code for jacquard.storage.base
"""Base class for storage engine implementations."""
import abc
import contextlib
from jacquard.storage.utils import TransactionMap
[docs]class StorageEngine(metaclass=abc.ABCMeta):
"""
Base storage engine class.
StorageEngine subclasses are required to be thread-safe.
"""
@abc.abstractmethod
def __init__(self, connection_string):
"""
Construct with a given connection string.
It is typical for the connection string to be a URL: even for files it
should generally take the form of a file: scheme URL.
"""
raise NotImplementedError
[docs] @abc.abstractmethod
def begin(self):
"""Enter a new transaction."""
raise NotImplementedError
[docs] def begin_read_only(self):
"""
Enter a new, read-only transaction.
May be overloaded for efficiency - by default, just calls begin().
"""
self.begin()
[docs] @abc.abstractmethod
def commit(self, changes, deletions):
"""
Commit transaction.
Writes to make are given in the two arguments: `changes` is a mapping
of keys to their new string values, and `deletions` is an iterable of
keys to remove entirely.
If optimistic locking or other transactional mechanisms fail, this can
raise the `Retry` exception to request that the entire transaction be
repeated.
"""
raise NotImplementedError
[docs] @abc.abstractmethod
def rollback(self):
"""
Roll back the current transaction without writing any changes.
This is used not only in exceptions but also when no writes were
necessary after a transaction, so should ideally be fairly fast.
"""
raise NotImplementedError
[docs] @abc.abstractmethod
def keys(self):
"""
Get an iterable over all keys in the store.
This is only ever called in transactions.
"""
raise NotImplementedError
[docs] @abc.abstractmethod
def get(self, key):
"""
Get the current value corresponding with a given key.
Where there is no current value this must return `None`.
Only ever called in a transaction.
"""
raise NotImplementedError
[docs] def encode_key(self, key):
"""
Convert a given key for use in the storage engine.
This optional method is given for engine-specific encodings of keys -
for instance, replacing slashes with the more idiomatic colons and
prefix for the Redis backend. All keys entering the rest of the API
are encoded.
The default implementation is the identity function.
"""
return key
[docs] def decode_key(self, key):
"""
Convert a given key for use in Jacquard.
This optional method must be implemented if `encode_key` is given,
and must be its inverse.
The default implementation is the identity function.
"""
return key
[docs] @contextlib.contextmanager
def transaction(self, read_only=False):
"""
Run a transactional sequence on this store.
This is (currently?) the main user API for `StorageEngine`. The
context manager yields an object supporting the mutable mapping
protocol in all its glory, which can be treated as if a dict in,
say, the standard library's `shelve` module.
When the context manager exits, the transaction is rolled back if
there are no writes or if an exception is escaping.
Commits are allowed to raise the `Retry` exception and it is left to
users of the API to deal with this. `Retry` is guaranteed not to be
raised if the transaction was read-only.
"""
if read_only:
self.begin_read_only()
else:
self.begin()
transaction_map = TransactionMap(self)
try:
yield transaction_map
except Exception:
self.rollback()
raise
if (
not transaction_map.changes and
not transaction_map.deletions
):
# Don't bother running a commit if nothing actually changed
self.rollback()
elif (
transaction_map.changes or
transaction_map.deletions
) and read_only:
self.rollback()
raise RuntimeError(
"Commit in read-only transaction (keys: {keys})".format(
keys=", ".join(
repr(x)
for x in (
set(transaction_map.changes.keys()) |
set(transaction_map.deletions)
)
),
)
)
else:
self.commit(
transaction_map.changes,
transaction_map.deletions,
)