Source code for ewoksutils.tests.test_logging_sqlite3

import logging
import threading
import time
from typing import Optional

from .. import sqlite3_utils
from ..logging_utils.sqlite3 import Sqlite3Handler
from ..sqlite3_utils import sqlite3

FIELD_TYPES = {"field1": 0, "field2": ""}


def _sqlite3_retry_period() -> Optional[float]:
    """Sqlite3Handler's native busy timeout blocks without yielding, which
    can deadlock a cooperative event loop (e.g. gevent) when the lock holder
    runs in another greenlet. Retry at the python level instead in that case.
    """
    try:
        from gevent.monkey import is_module_patched
    except ImportError:
        return None
    return 0.1 if is_module_patched("threading") else None


[docs] def test_concurrent_read_write(tmp_path): """Records emitted from several handlers while a reader is polling must all be inserted.""" uri = str(tmp_path / "test.db") nwriters = 4 nrecords = 20 # Barrier which ensures that reading and writing overlaps started = threading.Barrier(nwriters + 1, timeout=10) # Publish concurrently def write_records(value): handler = Sqlite3Handler(uri, "mytable", FIELD_TYPES) try: started.wait() for i in range(nrecords): handler.handle(_make_record(field1=value, field2=str(i))) finally: handler.close() writer_threads = [ threading.Thread(target=write_records, args=(value,)) for value in range(nwriters) ] # Read concurrently stop_reading = threading.Event() def read_records(): with sqlite3_utils.connect(uri, uri=True) as conn: started.wait() while not stop_reading.is_set(): try: _ = list( sqlite3_utils.select(conn, "mytable", field_types=FIELD_TYPES) ) except sqlite3.OperationalError: pass time.sleep(0.01) reader_thread = threading.Thread(target=read_records) reader_thread.start() try: for thread in writer_threads: thread.start() for thread in writer_threads: thread.join(timeout=60) finally: stop_reading.set() reader_thread.join(timeout=10) # Check that all records were published assert len(_select_all_records(uri)) == nwriters * nrecords
[docs] def test_write_blocked_by_reader(tmp_path): """A writer must wait for read locks to be released.""" retry_period = _sqlite3_retry_period() lock_seconds = 0.5 short_timeout = lock_seconds / 5 # do not retry long enough long_timeout = 5 * lock_seconds # retry long enough # Add record to the database uri = str(tmp_path / "test.db") handler = Sqlite3Handler(uri, "mytable", FIELD_TYPES, retry_period=retry_period) handler.handle(_make_record(field1=1)) handler.close() # Reader read_lock_acquired = threading.Event() def read_lock_database(): conn = sqlite3.connect(uri) try: # Start read transaction conn.execute("BEGIN") conn.execute("SELECT COUNT(*) FROM mytable").fetchone() read_lock_acquired.set() # Open read transaction locks the db time.sleep(lock_seconds) # Finish transaction conn.rollback() finally: conn.close() # Write while locked: do not retry long enough read_lock_acquired.clear() thread = threading.Thread(target=read_lock_database) thread.start() handler = Sqlite3Handler( uri, "mytable", FIELD_TYPES, timeout=short_timeout, retry_period=retry_period ) try: assert read_lock_acquired.wait(timeout=10) handler.handle(_make_record(field1=2)) finally: thread.join(timeout=10) handler.close() # Write while locked: retry long enough read_lock_acquired.clear() thread = threading.Thread(target=read_lock_database) thread.start() handler = Sqlite3Handler( uri, "mytable", FIELD_TYPES, timeout=long_timeout, retry_period=retry_period ) try: assert read_lock_acquired.wait(timeout=10) # Write while the reader locks handler.handle(_make_record(field1=3)) finally: thread.join(timeout=10) handler.close() # Check that the expected records were published records = _select_all_records(uri) assert [record["field1"] for record in records] == [1, 3]
def _make_record(**fields) -> logging.LogRecord: return logging.makeLogRecord(fields) def _select_all_records(uri: str) -> list: with sqlite3_utils.connect(uri, uri=True) as conn: return list(sqlite3_utils.select(conn, "mytable", field_types=FIELD_TYPES))