1.1rc3: more fsync

This commit is contained in:
AF 2023-01-13 15:33:04 +00:00
parent f3703c634e
commit dcc9d642aa
7 changed files with 94 additions and 45 deletions

4
.gitignore vendored
View File

@ -222,5 +222,5 @@ cython_debug/
# Other # Other
/dev.py /dev.py
/*.db *.db
/*.db.* *.db.*

View File

@ -5,8 +5,7 @@ import threading
from contextlib import ExitStack from contextlib import ExitStack
try: try:
sys.path.append('/app/') sys.path.append(str((pathlib.Path(__file__).parent / '../..').absolute()))
from ptvp35 import * from ptvp35 import *
from ptvp35.instrumentation import * from ptvp35.instrumentation import *
except: except:

View File

@ -6,7 +6,7 @@ This page describes reasons for certain design decisions.
General structure General structure
----------------- -----------------
* We're making a key-value database. * A key-value database.
* Keys and values are python objects. * Keys and values are python objects.
* While the DB is offline (after a correct shutdown), all its KVPs are stored as lines in one editable file. * While the DB is offline (after a correct shutdown), all its KVPs are stored as lines in one editable file.
* DB should be able to survive powercut at any point (D in ACID). * DB should be able to survive powercut at any point (D in ACID).

View File

@ -4,14 +4,70 @@ Inner structure
Main-Memory DataBase Main-Memory DataBase
-------------------- --------------------
* Represented via Python dictionary (:code:`dict`).
Future version may include more abstract options, i.e. :class:`ptvp35.DbConnection` would be generic over :attr:`ptvp35.DbConnection.__mmdb` field
* Keys and values are almost guaranteed to be serializable.
All KVPs loaded on :code:`__aenter__` are re-serialized during file reload.
All KVPs added via submit-like methods are serialized before being added to the MMDB.
DataBase Stream File DataBase Stream File
-------------------- --------------------
* In current implementation, all database storage storage files are Newline-Delimited JSON streams (https://en.wikipedia.org/wiki/JSON_streaming#Newline-Delimited_JSON).
.. code-block:: json
{"key": ["tuple", "example"], "value": {"dict": "example"}}
{"key": 123, "value": null}
* During the runtime, the database uses 6 different files:
* `.db` Main file.
Should be the only non-error file after correct shutdown.
* `.db.backup` Backup file.
Generated when the main file is being rebuilt.
* `.db.recover` Flag file.
Indicates that backup file is valid and that main file's validity is undefined.
* `.db.truncate` Auxiliary file created on each write to main file.
Contains 16 bytes little-endian representation of up to how many characters the main file is valid.
* `.db.truncate_flag` Flag file.
Indicates that truncate file is valid and that main file's validity after the specified character count is undefined.
* `.db.error` Error log file.
In current implementation, it contains only the main file contents that got truncated on recovery.
* All storage file writes are :code:`fsync`'ed.
* :code:`pathlib.Path.write_bytes` usecase relies on synchronisation/file-creation ordering. That may get replaced in future versions.
Request Queue Request Queue
------------- -------------
Transaction View Transaction View (:class:`ptvp35.TransactionView`)
---------------- --------------------------------------------------
Transaction Connection-like interface on top of another connection-like interface.
-----------
* Provides most of the same methods as :class:`ptvp35.DbConnection`.
* From the common :class:`ptvp35.VirtualConnection` interface/base class:
* :meth:`ptvp35.TransactionView.get`
* :meth:`ptvp35.TransactionView.commit_transaction`
* :meth:`ptvp35.TransactionView.submit_transaction_request`
* :meth:`ptvp35.TransactionView.loop`
* :meth:`ptvp35.TransactionView.transaction` (default implementation)
* Non-standard common methods:
* :meth:`ptvp35.TransactionView.set_nowait`
* :meth:`ptvp35.TransactionView.submit_transaction`
* :meth:`ptvp35.TransactionView.commit`
* Does not have the the analogue for the :meth:`ptvp35.DbConnection.set` method.
* The reason for that is :code:`set` method having semantics contradictory to transactions.
* The :code:`set` provides a way to set a *single* value and wait until it's committed.
* Transaction are meant for a more fine control.
* The equivalent would consist of the three method calls:
* :meth:`ptvp35.TransactionView.set_nowait` to set the value in :code:`__delta`.
* :meth:`ptvp35.TransactionView.submit` to pass all the :code:`__delta` values.
* :meth:`ptvp35.TransactionView.commit` wait until all changes are commited.
Transaction (:class:`ptvp35.Transaction`)
-----------------------------------------
Manages a Transaction View.
* Creates and returns a Transaction View on :code:`__aenter__`/:code:`__enter__`.
* Submits changes on successful :code:`__exit__`.
* Commits changes on successful :code:`__aexit__`.
* Rolls back changes on unsuccessful :code:`__exit__`/:code:`__aexit__`.

View File

@ -13,12 +13,10 @@ Default installation option is to use pip+git
Basic functionality Basic functionality
------------------- -------------------
.. autoclass:: ptvp35.DbFactory :class:`ptvp35.DbFactory` class provides context management for database connections (:class:`ptvp35.DbConnection`) via :code:`async with` statement.
The connection isn't just a "connection", it's also the MMDB itself, so **using two connections to one database is an undefined behaviour**.
:code:`DbFactory` class provides context management for database connections (:code:`DbConnection`) via :code:`async with` statement. Also, that means that each connection start/shutdown is quite time expensive.
The connection isn't just a "connection", it's also the MMDB itself, so **using two connections to one database is an undefined behaviour**. These two facts together tell that, if you intend on using the connection, you should probably wrap the main program in an :code:`async with` block.
Also, that means that each connection start/shutdown is quite time expensive.
These two facts together tell that, if you intend on using the connection, you should probably wrap the main program in an :code:`async with` block.
.. code-block:: python3 .. code-block:: python3
@ -62,26 +60,16 @@ Different ways to get/set a value:
transaction.set_nowait('increment-5', value5 + 1) transaction.set_nowait('increment-5', value5 + 1)
await connection.commit() await connection.commit()
.. autoclass:: ptvp35.DbConnection * :meth:`ptvp35.DbConnection.get`
.. automethod:: get
this method is instant. this method is instant.
* :meth:`ptvp35.DbConnection.set`
.. automethod:: set
this method may take time to run. this method may take time to run.
ordering may not be guaranteed (depends on event loop implementation). ordering may not be guaranteed (depends on event loop implementation).
* :meth:`ptvp35.DbConnection.set_nowait`
.. automethod:: set_nowait
this method is instant. this method is instant.
ordering is guaranteed. ordering is guaranteed.
* :meth:`ptvp35.DbConnection.commit`
.. automethod:: commit
this method may take time to run. this method may take time to run.
respects the ordering of previously called :code:`set_nowait` methods. respects the ordering of previously called :code:`set_nowait` methods.
will, under most circumstances, also execute later changes. will, depending on event loop implementation, also execute later changes.
* :meth:`ptvp35.DbConnection.transaction`
.. automethod:: transaction

View File

@ -262,7 +262,6 @@ class VirtualConnection(abc.ABC):
def loop(self, /) -> asyncio.AbstractEventLoop: def loop(self, /) -> asyncio.AbstractEventLoop:
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def transaction(self, /) -> 'Transaction': def transaction(self, /) -> 'Transaction':
return Transaction(self) return Transaction(self)
@ -342,7 +341,9 @@ class DbConnection(VirtualConnection):
def _db2path_sync(self, db: dict, path: pathlib.Path, /) -> int: def _db2path_sync(self, db: dict, path: pathlib.Path, /) -> int:
with path.open('w') as file: with path.open('w') as file:
return self.__kvfactory.db2io(db, file) initial_size = self.__kvfactory.db2io(db, file)
os.fsync(file.fileno())
return initial_size
def get(self, key: Any, default: Any, /): def get(self, key: Any, default: Any, /):
"""dict-like get with mandatory default parametre.""" """dict-like get with mandatory default parametre."""
@ -430,10 +431,15 @@ class DbConnection(VirtualConnection):
self.__buffer.write(line) self.__buffer.write(line)
await self._commit_buffer_or_request_so(request) await self._commit_buffer_or_request_so(request)
def _write_truncation_bytes(self, s: bytes, /) -> None:
# consider subclassing/rewriting to use `os.fsync`
self.__path_truncate.write_bytes(s)
def _write_truncation_value(self, value: int, /) -> None:
self._write_truncation_bytes(value.to_bytes(16, 'little'))
def _truncation_set_sync(self, /) -> None: def _truncation_set_sync(self, /) -> None:
self.__path_truncate.write_bytes( self._write_truncation_value(self.__file.tell())
self.__file.tell().to_bytes(16, 'little')
)
self.__path_truncate_flag.touch() self.__path_truncate_flag.touch()
def _truncation_unset_sync(self, /) -> None: def _truncation_unset_sync(self, /) -> None:
@ -695,7 +701,7 @@ note: unstable signature."""
return self.__loop return self.__loop
def transaction(self, /) -> 'Transaction': def transaction(self, /) -> 'Transaction':
return Transaction(self) return super().transaction()
class DbFactory: class DbFactory:

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name='ptvp35', name='ptvp35',
version='1.1rc2', version='1.1rc3',
packages=['ptvp35'], packages=['ptvp35'],
url='https://gitea.ongoteam.net/PTV/ptvp35', url='https://gitea.ongoteam.net/PTV/ptvp35',
license='MIT', license='MIT',