You've already forked mariadb-columnstore-engine
							
							
				mirror of
				https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
				synced 2025-11-03 17:13:17 +03:00 
			
		
		
		
	* feat(cmapi): add read_only param for API add node endpoint * style(cmapi): fixes for string length and quotes Add dbroots of other nodes to the read-only node On every node change adjust dbroots in the read-only nodes Fix logging (trace level) in tests Remove ExeMgr from constants Fix tests Manually remove read-only node from ReadOnlyNodes on node removal (because nodes are only deactivated) Review fixes (mostly switching to StrEnum analog before py3.11, also changes in ruff config) Read-only nodes are now called read replica consistently Don't write hostname into IP fields of the config like PMSx/IPAddr, pmx_WriteEngineServer/IPAddr We calculate ReadReplicas by finding PMs without WriteEngineServer In _replace_localhost, replace local IP addrs with resolved IP addrs and local hostnames -- with the resolved hostnames. ModuleHostName/ModuleIPAddr is kept intact. Keep only IPv4 in ActiveNodes/DesiredNodes/InactiveNodes feat: add mock DNS resolution builder for testing hostname/IP mappings * Fix _add_node_to_PMS: if node is already in PMS, save it to existing items to not miss it during the reconstruction of the list * Make tests independent from CWD Fixed for _add_Module_entries Fixed node removal and tests Fixes for node manipulation tests
		
			
				
	
	
		
			175 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Module related to CMAPI transaction management logic."""
 | 
						|
import logging
 | 
						|
from contextlib import ContextDecorator
 | 
						|
from signal import (
 | 
						|
    SIGINT, SIGTERM, SIGHUP, SIG_DFL, signal, default_int_handler
 | 
						|
)
 | 
						|
from typing import Optional
 | 
						|
 | 
						|
from cmapi_server.constants import DEFAULT_MCS_CONF_PATH, TRANSACTION_TIMEOUT
 | 
						|
from cmapi_server.exceptions import CMAPIBasicError
 | 
						|
from cmapi_server.helpers import (
 | 
						|
    get_id, commit_transaction, rollback_transaction, start_transaction
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
class TransactionManager(ContextDecorator):
 | 
						|
    """Context manager and decorator to put any code inside CMAPI transaction.
 | 
						|
 | 
						|
    :param timeout: time in sec after transaction will be autocommitted,
 | 
						|
                    defaults to 300.0 (TRANSACTION_TIMEOUT)
 | 
						|
 | 
						|
    :param timeout: _description_, defaults to 300
 | 
						|
    :type timeout: float, optional
 | 
						|
    :param txn_id: custom transaction id, defaults to None
 | 
						|
    :type txn_id: Optional[int], optional
 | 
						|
    :param handle_signals: handle specific signals or not, defaults to False
 | 
						|
    :type handle_signals: bool, optional
 | 
						|
    :param extra_nodes: extra nodes to start transaction at, defaults to None
 | 
						|
    :type extra_nodes: Optional[list], optional
 | 
						|
    :param remove_nodes: nodes to remove from transaction, defaults to None
 | 
						|
    :type remove_nodes: Optional[list], optional
 | 
						|
    :param optional_nodes: nodes to add to transaction, defaults to None
 | 
						|
    :type optional_nodes: Optional[list], optional
 | 
						|
    :raises CMAPIBasicError: if there are no nodes in the cluster
 | 
						|
    :raises CMAPIBasicError: if starting transaction isn't succesful
 | 
						|
    :raises Exception: if error while starting the transaction
 | 
						|
    :raises Exception: if error while committing transaction
 | 
						|
    :raises Exception: if error while rollback transaction
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
            self, timeout: float = TRANSACTION_TIMEOUT,
 | 
						|
            txn_id: Optional[int] = None, handle_signals: bool = False,
 | 
						|
            extra_nodes: Optional[list] = None,
 | 
						|
            remove_nodes: Optional[list] = None,
 | 
						|
            optional_nodes: Optional[list] = None,
 | 
						|
    ):
 | 
						|
        self.timeout = timeout
 | 
						|
        self.txn_id = txn_id or get_id()
 | 
						|
        self.handle_signals = handle_signals
 | 
						|
        self.active_transaction = False
 | 
						|
        self.extra_nodes = extra_nodes
 | 
						|
        self.remove_nodes = remove_nodes
 | 
						|
        self.optional_nodes = optional_nodes
 | 
						|
        self.success_txn_nodes = None
 | 
						|
 | 
						|
    def _handle_exception(
 | 
						|
            self, exc: Optional[Exception] = None,
 | 
						|
            signum: Optional[int] = None
 | 
						|
    ) -> None:
 | 
						|
        """Handle raised exceptions.
 | 
						|
 | 
						|
        We need to rollback transaction in some cases and return back default
 | 
						|
        signal handlers.
 | 
						|
 | 
						|
        :param exc: exception passed, defaults to None
 | 
						|
        :type exc: Optional[Type[Exception]], optional
 | 
						|
        :param signum: signal if it cause exception, defaults to None
 | 
						|
        :type signum: Optional[int], optional
 | 
						|
        :raises exc: raises passed exception
 | 
						|
        """
 | 
						|
        # message = 'Got exception in transaction manager'
 | 
						|
        if (exc or signum) and self.active_transaction:
 | 
						|
            self.rollback_transaction(nodes=self.success_txn_nodes)
 | 
						|
        self.set_default_signals()
 | 
						|
        if exc is not None:
 | 
						|
            raise exc
 | 
						|
 | 
						|
    def _handle_signal(self, signum, frame) -> None:
 | 
						|
        """Handler for signals.
 | 
						|
 | 
						|
        :param signum: signal number
 | 
						|
        :type signum: int
 | 
						|
        """
 | 
						|
        logging.error(f'Caught signal "{signum}" in transaction manager.')
 | 
						|
        self._handle_exception(signum=signum)
 | 
						|
 | 
						|
    def set_custom_signals(self) -> None:
 | 
						|
        """Set handlers for several signals."""
 | 
						|
        # register handler for signals for proper handling them
 | 
						|
        for sig in SIGINT, SIGTERM, SIGHUP:
 | 
						|
            signal(sig, self._handle_signal)
 | 
						|
 | 
						|
    def set_default_signals(self) -> None:
 | 
						|
        """Return defalt handlers for specific signals."""
 | 
						|
        if self.handle_signals:
 | 
						|
            signal(SIGINT, default_int_handler)
 | 
						|
            signal(SIGTERM, SIG_DFL)
 | 
						|
            signal(SIGHUP, SIG_DFL)
 | 
						|
 | 
						|
    def rollback_transaction(self, nodes: Optional[list] = None) -> None:
 | 
						|
        """Rollback transaction.
 | 
						|
 | 
						|
        :param nodes: nodes to rollback transaction, defaults to None
 | 
						|
        :type nodes: Optional[list], optional
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            rollback_transaction(self.txn_id, nodes=nodes)
 | 
						|
            self.active_transaction = False
 | 
						|
            logging.debug(f'Successful rollback of transaction "{self.txn_id}".')
 | 
						|
        except Exception:
 | 
						|
            logging.error(
 | 
						|
                f'Error while rolling back transaction "{self.txn_id}"',
 | 
						|
                exc_info=True
 | 
						|
            )
 | 
						|
 | 
						|
    def commit_transaction(self, nodes: Optional[list] = None) -> None:
 | 
						|
        """Commit transaction.
 | 
						|
 | 
						|
        :param nodes: nodes to commit transaction, defaults to None
 | 
						|
        :type nodes: Optional[list], optional
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            commit_transaction(
 | 
						|
                self.txn_id, cs_config_filename=DEFAULT_MCS_CONF_PATH,
 | 
						|
                nodes=nodes
 | 
						|
            )
 | 
						|
        except Exception:
 | 
						|
            logging.error(f'Error while committing transaction {self.txn_id}')
 | 
						|
            self.rollback_transaction(nodes=self.success_txn_nodes)
 | 
						|
            self.set_default_signals()
 | 
						|
            raise
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        if self.handle_signals:
 | 
						|
            self.set_custom_signals()
 | 
						|
        suceeded, success_txn_nodes = False, []
 | 
						|
        try:
 | 
						|
            suceeded, _, success_txn_nodes = start_transaction(
 | 
						|
                cs_config_filename=DEFAULT_MCS_CONF_PATH,
 | 
						|
                extra_nodes=self.extra_nodes, remove_nodes=self.remove_nodes,
 | 
						|
                optional_nodes=self.optional_nodes,
 | 
						|
                txn_id=self.txn_id, timeout=self.timeout,
 | 
						|
            )
 | 
						|
        except Exception as exc:
 | 
						|
            logging.error('Error while starting the transaction.')
 | 
						|
            self._handle_exception(exc=exc)
 | 
						|
        if not suceeded:
 | 
						|
            self._handle_exception(
 | 
						|
                exc=CMAPIBasicError('Starting transaction isn\'t succesful.')
 | 
						|
            )
 | 
						|
        if suceeded and len(success_txn_nodes) == 0:
 | 
						|
            # corner case when deleting last node in the cluster
 | 
						|
            # TODO: remove node mechanics potentially has a vulnerability
 | 
						|
            #       because no transaction started for removing node.
 | 
						|
            #       Probably in some cases rollback never works for removing
 | 
						|
            #       node, because it never exist in success_txn_nodes.
 | 
						|
            if not self.remove_nodes:
 | 
						|
                self._handle_exception(
 | 
						|
                    exc=CMAPIBasicError('There are no nodes in the cluster.')
 | 
						|
                )
 | 
						|
        self.active_transaction = True
 | 
						|
        self.success_txn_nodes = success_txn_nodes
 | 
						|
        return self
 | 
						|
 | 
						|
    def __exit__(self, *exc):
 | 
						|
        if exc[0] and self.active_transaction:
 | 
						|
            self.rollback_transaction(nodes=self.success_txn_nodes)
 | 
						|
            self.set_default_signals()
 | 
						|
            return False
 | 
						|
        if self.active_transaction:
 | 
						|
            self.commit_transaction(nodes=self.success_txn_nodes)
 | 
						|
            self.set_default_signals()
 | 
						|
        return True
 |