mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
231 lines
7 KiB
Python
231 lines
7 KiB
Python
import asyncio
|
|
import contextlib
|
|
import multiprocessing
|
|
import subprocess
|
|
from datetime import datetime
|
|
from typing import List
|
|
|
|
from sqlalchemy import Column, Boolean, Integer
|
|
|
|
from wrolpi.common import logger, Base
|
|
|
|
__all__ = [
|
|
'get_flags',
|
|
'refreshing',
|
|
'refresh_complete',
|
|
]
|
|
|
|
from wrolpi.vars import PYTEST
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
TESTING_LOCK = multiprocessing.Event()
|
|
|
|
FLAG_NAMES = set()
|
|
|
|
|
|
class Flag:
|
|
"""A simple wrapper around multiprocessing.Event.
|
|
|
|
This allows synchronization between the App and this API.
|
|
|
|
This may store its value in the DB table wrolpi_flag."""
|
|
|
|
def __init__(self, name: str, store_db: bool = False):
|
|
self.name = name
|
|
self.store_db = store_db
|
|
FLAG_NAMES.add(name)
|
|
|
|
def __repr__(self):
|
|
return f'<Flag {repr(self.name)}>'
|
|
|
|
def set(self):
|
|
"""Set the multiprocessing.Event for this Flag."""
|
|
if PYTEST and not TESTING_LOCK.is_set():
|
|
# Testing, but the test does not need flags.
|
|
return
|
|
|
|
from wrolpi.api_utils import api_app
|
|
api_app.shared_ctx.flags.update({self.name: True})
|
|
self._save(True)
|
|
|
|
def clear(self):
|
|
"""Clear the multiprocessing.Event for this Flag."""
|
|
if PYTEST and not TESTING_LOCK.is_set():
|
|
# Testing, but the test does not need flags.
|
|
return
|
|
|
|
from wrolpi.api_utils import api_app
|
|
api_app.shared_ctx.flags.update({self.name: False})
|
|
self._save(False)
|
|
|
|
def is_set(self):
|
|
"""Return True if this multiprocessing.Event is set."""
|
|
if PYTEST and not TESTING_LOCK.is_set():
|
|
# Testing, but the test does not need flags.
|
|
return
|
|
|
|
from wrolpi.api_utils import api_app
|
|
return api_app.shared_ctx.flags[self.name]
|
|
|
|
def __enter__(self):
|
|
if PYTEST and not TESTING_LOCK.is_set():
|
|
# Testing, but the test does not need flags.
|
|
return
|
|
|
|
if self.is_set():
|
|
raise ValueError(f'{self} flag is already set!')
|
|
self.set()
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if PYTEST and not TESTING_LOCK.is_set():
|
|
# Testing, but the test does not need flags.
|
|
return
|
|
|
|
self.clear()
|
|
|
|
def _save(self, value: bool):
|
|
if self.store_db:
|
|
from wrolpi.db import get_db_curs
|
|
# Store the value of this Flag in it's matching column in the DB. Not all flags will do this.
|
|
try:
|
|
with get_db_curs(commit=True) as curs:
|
|
# Upsert the only row in the table.
|
|
curs.execute(f'''
|
|
INSERT INTO wrolpi_flag (id, {self.name}) VALUES (1, %(value)s)
|
|
ON CONFLICT (id) DO UPDATE SET {self.name} = %(value)s
|
|
''', dict(value=value))
|
|
except Exception as e:
|
|
logger.critical(f'Unable to save flag! {repr(self)}', exc_info=e)
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def wait_for(self, timeout: int = 0):
|
|
"""Wait for this Flag to be set."""
|
|
async with wait_for_flag(self, timeout=timeout):
|
|
yield
|
|
|
|
|
|
# Set by `check_db_is_up`.
|
|
db_up = Flag('db_up')
|
|
# The global refresh is running.
|
|
refreshing = Flag('refreshing')
|
|
# Outdated Zims need to be removed.
|
|
outdated_zims = Flag('outdated_zims', store_db=True)
|
|
# Kiwix server needs to be restarted.
|
|
kiwix_restart = Flag('kiwix_restart')
|
|
# Map Importing
|
|
map_importing = Flag('map_importing')
|
|
|
|
# Steps of refreshing.
|
|
refresh_counting = Flag('refresh_counting')
|
|
refresh_discovery = Flag('refresh_discovery')
|
|
refresh_modeling = Flag('refresh_modeling')
|
|
refresh_indexing = Flag('refresh_indexing')
|
|
refresh_cleanup = Flag('refresh_cleanup')
|
|
|
|
# The global refresh has been performed. This is False on a fresh instance of WROLPi.
|
|
refresh_complete = Flag('refresh_complete', store_db=True)
|
|
|
|
|
|
def get_flags() -> List[str]:
|
|
"""Return a list of all Flags which are set."""
|
|
flags = []
|
|
if db_up.is_set():
|
|
flags.append('db_up')
|
|
if refreshing.is_set():
|
|
flags.append('refreshing')
|
|
if refresh_complete.is_set():
|
|
flags.append('refresh_complete')
|
|
if outdated_zims.is_set():
|
|
flags.append('outdated_zims')
|
|
if kiwix_restart.is_set():
|
|
flags.append('kiwix_restart')
|
|
return flags
|
|
|
|
|
|
class WROLPiFlag(Base):
|
|
__tablename__ = 'wrolpi_flag'
|
|
id = Column(Integer, primary_key=True)
|
|
refresh_complete = Column(Boolean, default=False)
|
|
outdated_zims = Column(Boolean, default=False)
|
|
|
|
def __repr__(self):
|
|
return f'<WROLPiFlag' \
|
|
f' refresh_complete={self.refresh_complete}' \
|
|
f' outdated_zims={self.outdated_zims}>'
|
|
|
|
|
|
def check_db_is_up():
|
|
"""Attempts to connect to the database, sets flags.db_up if successful."""
|
|
from wrolpi.db import get_db_curs, get_db_args
|
|
|
|
db_args = get_db_args()
|
|
db_host = db_args['host']
|
|
if db_host != '127.0.0.1':
|
|
# Check if database host is up before attempting connection. This will fail faster than SQLAlchemy timeout.
|
|
try:
|
|
cmd = ['ping', '-w', '1', '-c', '1', db_host]
|
|
subprocess.check_call(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
except Exception as e:
|
|
logger.debug(f'Unable to resolve database host {db_host}', exc_info=e)
|
|
db_up.clear()
|
|
return
|
|
|
|
try:
|
|
with get_db_curs() as curs:
|
|
curs.execute('SELECT * FROM alembic_version')
|
|
# If we get here, the database is up!
|
|
db_up.set()
|
|
except Exception as e:
|
|
logger.debug(f'DB is not up', exc_info=e)
|
|
db_up.clear()
|
|
|
|
|
|
def init_flags():
|
|
"""Read flag values from the DB, copy them to the shared context flags."""
|
|
if not db_up.is_set():
|
|
logger.error(f'Refusing to initialize flags when DB is not up.')
|
|
return
|
|
|
|
from wrolpi.api_utils import api_app
|
|
if api_app.shared_ctx.flags_initialized.is_set():
|
|
# Only need to read from once DB at startup.
|
|
return
|
|
|
|
logger.debug('Initializing flags...')
|
|
|
|
from wrolpi.db import get_db_session
|
|
with get_db_session() as session:
|
|
flags: WROLPiFlag = session.query(WROLPiFlag).one_or_none()
|
|
if flags:
|
|
if flags.refresh_complete is True:
|
|
refresh_complete.set()
|
|
else:
|
|
refresh_complete.clear()
|
|
if flags.outdated_zims is True:
|
|
outdated_zims.set()
|
|
else:
|
|
outdated_zims.clear()
|
|
|
|
logger.debug('Initialized flags')
|
|
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def wait_for_flag(flag: Flag, timeout: int = 0):
|
|
"""Sleeps until the provided Flag is set.
|
|
|
|
>>> async with wait_for_flag(db_up):
|
|
>>> do_db_operation()
|
|
|
|
@raise TimeoutError: Raises this when the timeout is reached.
|
|
"""
|
|
start = datetime.now()
|
|
while True:
|
|
if timeout and (elapsed := (datetime.now() - start).total_seconds()) > timeout:
|
|
raise TimeoutError(f'Waited too long ({elapsed}s) for {flag} to be set!')
|
|
|
|
if flag.is_set():
|
|
break
|
|
await asyncio.sleep(0.1)
|
|
|
|
yield
|