Work in progress on state store.
This commit is contained in:
parent
6f00ca34cd
commit
e5dbf96cb4
|
@ -3,42 +3,62 @@
|
|||
PostgreSQL cluster and that reconfigures pgbouncer when needed."""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from pgbouncemgr.logger import Logger
|
||||
from pgbouncemgr.logger import Logger, ConsoleLogTarget, SyslogLogTarget
|
||||
from pgbouncemgr.config import Config
|
||||
from pgbouncemgr.state import State
|
||||
from pgbouncemgr.state_store import StateStore
|
||||
|
||||
|
||||
DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml"
|
||||
DEFAULT_LOG_FACILITY = "LOG_LOCAL1"
|
||||
|
||||
|
||||
def main():
|
||||
"""Starts the pgbouncemgr main application."""
|
||||
args = _parse_arguments()
|
||||
config = Config(args.config)
|
||||
log = Logger("pgbouncemgr", args.log_facility, args.debug, args.verbose)
|
||||
class Manager():
|
||||
def __init__(self, argv=None):
|
||||
args = _parse_arguments(argv)
|
||||
self.config = Config(args.config)
|
||||
self._create_logger(args)
|
||||
self._create_state()
|
||||
|
||||
def _create_logger(self, args):
|
||||
self.log = Logger()
|
||||
self.log.append(ConsoleLogTarget(args.verbose, args.debug))
|
||||
if args.log_facility.lower() != 'none':
|
||||
self.log.append(SyslogLogTarget("pgbouncemgr", args.log_facility))
|
||||
|
||||
def _create_state(self):
|
||||
self.state = State.fromConfig(self.config, self.log)
|
||||
self.state_store = StateStore(self.config.state_file, self.state)
|
||||
self.state_store.load()
|
||||
|
||||
def start(self):
|
||||
self.log.info("Not yet!")
|
||||
self.log.debug("Work in progres...")
|
||||
self.log.warning("Beware!")
|
||||
self.log.error("I will crash now")
|
||||
|
||||
|
||||
def _parse_arguments():
|
||||
def _parse_arguments(args):
|
||||
parser = ArgumentParser(description="pgbouncemgr")
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
default=False, action="store_true",
|
||||
help="enable verbose output (default: disabled)")
|
||||
help="enable verbose console output (default: disabled)")
|
||||
parser.add_argument(
|
||||
"-d", "--debug",
|
||||
default=False, action="store_true",
|
||||
help="enable debugging output (default: disabled)")
|
||||
help="enable debugging console output (default: disabled)")
|
||||
parser.add_argument(
|
||||
"-f", "--log-facility",
|
||||
default=DEFAULT_LOG_FACILITY,
|
||||
help="syslog facility to use (default: %s)" % DEFAULT_LOG_FACILITY)
|
||||
help="syslog facility to use or 'none' to disable syslog logging " +
|
||||
"(default: %s)" % DEFAULT_LOG_FACILITY)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG,
|
||||
help="config file (default: %s)" % DEFAULT_CONFIG)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Manager(None).start()
|
||||
|
|
|
@ -7,6 +7,10 @@ class NodeConfig():
|
|||
self._pgbouncer_config = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.connect_timeout = 1
|
||||
self.name = 'template1'
|
||||
self.user = 'pgbouncemgr'
|
||||
self.password = None
|
||||
|
||||
@property
|
||||
def pgbouncer_config(self):
|
||||
|
@ -21,6 +25,8 @@ class NodeConfig():
|
|||
self._pgbouncer_config = path
|
||||
|
||||
def export(self):
|
||||
"""Exports the data for the node configuration, that we want
|
||||
to end up in the state data."""
|
||||
return {
|
||||
"pgbouncer_config": self.pgbouncer_config,
|
||||
"host": self.host,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# no-pylint: disable=missing-docstring,broad-except,too-many-instance-attributes
|
||||
|
||||
import os
|
||||
from pgbouncemgr.node_config import NodeConfig
|
||||
|
||||
|
||||
|
@ -85,6 +86,20 @@ class InvalidLeaderStatus(StateException):
|
|||
("The leader_status cannot be set to '%s' " +
|
||||
"(valid values are: %s)") % (status, ", ".join(LEADER_STATUSES)))
|
||||
|
||||
class UnknownLeaderNodeId(StateException):
|
||||
"""Raised when leader_Node_id is set to a non-existing node_id value."""
|
||||
def __init__(self, leader_node_id):
|
||||
super().__init__(
|
||||
"The leader_node_id does not exist " +
|
||||
"(leader_node_id=%s)" % leader_node_id)
|
||||
|
||||
class ActivePgbouncerConfigDoesNotExist(StateException):
|
||||
"""Raised when active_pgbouncer_config is set to a non-existing path."""
|
||||
def __init__(self, active_pgbouncer_config):
|
||||
super().__init__(
|
||||
"The active_pgbouncer_config file does not exist " +
|
||||
"(path=%s)" % active_pgbouncer_config)
|
||||
|
||||
class InvalidNodeStatus(StateException):
|
||||
"""Raised when the status for a node is set to an invalid value."""
|
||||
def __init__(self, status):
|
||||
|
@ -93,12 +108,23 @@ class InvalidNodeStatus(StateException):
|
|||
"(valid values are: %s)") % (status, ", ".join(NODE_STATUSES)))
|
||||
|
||||
class State():
|
||||
def __init__(self):
|
||||
@staticmethod
|
||||
def fromConfig(config, logger):
|
||||
state = State(logger)
|
||||
for node_id, settings in config.nodes.items():
|
||||
node_config = NodeConfig(node_id)
|
||||
for k, v in settings.items():
|
||||
setattr(node_config, k, v)
|
||||
state.add_node(node_config)
|
||||
return state
|
||||
|
||||
def __init__(self, logger):
|
||||
self.log = logger
|
||||
self.modified = False
|
||||
self._system_id = None
|
||||
self._timeline_id = None
|
||||
self._active_pgbouncer_config = None
|
||||
self.leader_node_id = None
|
||||
self._leader_node_id = None
|
||||
self._leader_error = None
|
||||
self._leader_status = LEADER_UNKNOWN
|
||||
self.nodes = {}
|
||||
|
@ -204,6 +230,26 @@ class State():
|
|||
self._leader_status = status
|
||||
self.modified = True
|
||||
|
||||
@property
|
||||
def leader_node_id(self):
|
||||
return self._leader_node_id
|
||||
|
||||
@leader_node_id.setter
|
||||
def leader_node_id(self, leader_node_id):
|
||||
"""Sets the id of the leader node."""
|
||||
try:
|
||||
node = self.get_node(leader_node_id)
|
||||
except UnknownNodeRequested:
|
||||
self.log.warning(
|
||||
"The laeder_node_id value points to a non-existing " +
|
||||
"node id: node config changed? (leader_node_id=%s)" %
|
||||
leader_node_id)
|
||||
raise UnknownLeaderNodeId(leader_node_id)
|
||||
if self._leader_node_id == leader_node_id:
|
||||
return
|
||||
self._leader_node_id = leader_node_id
|
||||
self.modified = True
|
||||
|
||||
@property
|
||||
def leader_error(self):
|
||||
return self._leader_error
|
||||
|
@ -223,13 +269,22 @@ class State():
|
|||
return self._active_pgbouncer_config
|
||||
|
||||
@active_pgbouncer_config.setter
|
||||
def active_pgbouncer_config(self, pgbouncer_config):
|
||||
def active_pgbouncer_config(self, active_pgbouncer_config):
|
||||
"""Sets the pgbouncer configuration file that must be activated
|
||||
in order to connect pgbouncer to the correct backend
|
||||
PostgreSQL server."""
|
||||
if self._active_pgbouncer_config == pgbouncer_config:
|
||||
PostgreSQL server.
|
||||
When the active config is set to a non-existing file (this can
|
||||
happen when the node configuration changes), a warning will be
|
||||
logged."""
|
||||
if not os.path.exists(active_pgbouncer_config):
|
||||
self.log.warning(
|
||||
"The active_pgbouncer_config value points to a non-existing " +
|
||||
"file: node config changed? (path=%s)" %
|
||||
active_pgbouncer_config)
|
||||
raise ActivePgbouncerConfigDoesNotExist(active_pgbouncer_config)
|
||||
if self._active_pgbouncer_config == active_pgbouncer_config:
|
||||
return
|
||||
self._active_pgbouncer_config = pgbouncer_config
|
||||
self._active_pgbouncer_config = active_pgbouncer_config
|
||||
self.modified = True
|
||||
|
||||
def export(self):
|
||||
|
|
|
@ -6,6 +6,8 @@ from datetime import datetime
|
|||
from os import rename
|
||||
from os.path import isfile
|
||||
from pgbouncemgr.logger import format_ex
|
||||
from pgbouncemgr.state import \
|
||||
UnknownLeaderNodeId, ActivePgbouncerConfigDoesNotExist
|
||||
|
||||
|
||||
class StateStoreException(Exception):
|
||||
|
@ -44,7 +46,18 @@ class StateStore():
|
|||
"timeline_id",
|
||||
"active_pgbouncer_config",
|
||||
"leader_node_id"]:
|
||||
setattr(self.state, key, loaded_state[key])
|
||||
if key in loaded_state:
|
||||
try:
|
||||
setattr(self.state, key, loaded_state[key])
|
||||
except (UnknownLeaderNodeId,
|
||||
ActivePgbouncerConfigDoesNotExist):
|
||||
# These exception can occur when the node configuration
|
||||
# has changed and some fields are no longer value.
|
||||
# In such case, we don't update the attributes here
|
||||
# and simply ignore the specific errors. After the
|
||||
# application has started, the correct values will be
|
||||
# filled by the manager process.
|
||||
pass
|
||||
|
||||
def store(self):
|
||||
"""Store the current state in the state file. Returns True when
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pgbouncer.manager import DEFAULT_CONFIG, DEFAULT_LOG_FACILITY
|
||||
|
||||
|
||||
class ArgsStub():
|
||||
config = DEFAULT_CONFIG
|
||||
log_facility = DEFAULT_LOG_FACILITY
|
||||
verbose = False
|
||||
debug = False
|
||||
|
||||
|
||||
class LoggerStub():
|
||||
def __init(self, ident, facility, debug, verbose):
|
||||
pass
|
||||
|
||||
def debug(self, msg):
|
||||
pass
|
||||
|
||||
def info(self, msg):
|
||||
pass
|
||||
|
||||
def warning(self, msg):
|
||||
pass
|
||||
|
||||
def error(self, msg):
|
||||
pass
|
||||
|
||||
def format_ex(self, exception):
|
||||
return "FMTEX"
|
||||
|
||||
|
||||
class StateStoreStub():
|
||||
old_state = None
|
||||
test_with_nr_of_errors = 0
|
||||
test_with_state = None
|
||||
|
||||
def load(self)
|
||||
state = State()
|
||||
|
||||
def store(self, state):
|
||||
new_state = json.dumps(state_to_store, sort_keys=True, indent=2)
|
||||
if self.old_state and self.old_state == new_state:
|
||||
return STORE_NOCHANGE
|
||||
sel.old_state = new_state
|
||||
|
||||
if test_with_nr_of_errors:
|
||||
test_with_nr_of_errors -= 1
|
||||
self.err = "Storing state failed: just testing"
|
||||
return STORE_ERROR
|
||||
|
||||
return STORE_UPDATED
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from pgbouncemgr.manager import *
|
||||
|
||||
|
||||
CONFIG = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"testfiles", "basic.yaml")
|
||||
|
||||
CONFIG_WITH_STATE_FILE = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"testfiles", "basic_with_state_file.yaml")
|
||||
|
||||
STATE_FILE_TEMPLATE = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"testfiles", "state.json")
|
||||
|
||||
STATE_FILE = "/tmp/pgbouncemgr.state"
|
||||
|
||||
|
||||
class ManagerTests(unittest.TestCase):
|
||||
def test_CreateManagerUsingCommandLineOptions(self):
|
||||
"""Check if a Manager object can be created, making use of some
|
||||
command line arguments."""
|
||||
mgr = Manager([
|
||||
'-f', 'none', # no syslog
|
||||
'-d', # debug (and verbose) enabled
|
||||
'--config', CONFIG, # the config file to use
|
||||
'-f', 'none' # the syslog facility to use, 'none' disables syslog
|
||||
])
|
||||
|
||||
# Check the logging setup.
|
||||
self.assertEqual(1, len(mgr.log))
|
||||
console = mgr.log[0]
|
||||
self.assertEqual('ConsoleLogTarget', type(console).__name__)
|
||||
self.assertTrue(console.verbose_enabled)
|
||||
self.assertTrue(console.debug_enabled)
|
||||
|
||||
def test_GivenNoStateFile_WhenCreatingManager_DefaultsAreUsed(self):
|
||||
mgr = Manager(['--config', CONFIG])
|
||||
|
||||
# Check if the config file was read.
|
||||
# If yes, then the state should be filled with data for the
|
||||
# the configured nodes and all state should be in the initial
|
||||
# configuration.
|
||||
self.maxDiff = 5000
|
||||
self.assertEqual({
|
||||
'system_id': None,
|
||||
'timeline_id': None,
|
||||
'active_pgbouncer_config': None,
|
||||
'leader_error': None,
|
||||
'leader_node_id': None,
|
||||
'leader_status': 'LEADER_UNKNOWN',
|
||||
'nodes': {
|
||||
'nodeA': {
|
||||
'config': {
|
||||
'host': '1.2.3.4',
|
||||
'port': 8888,
|
||||
'pgbouncer_config': None
|
||||
},
|
||||
'node_id': 'nodeA',
|
||||
'is_leader': False,
|
||||
'system_id': None,
|
||||
'timeline_id': None,
|
||||
'status': 'NODE_UNKNOWN',
|
||||
'error': None
|
||||
},
|
||||
'nodeB': {
|
||||
'config': {
|
||||
'host': '2.3.4.5',
|
||||
'port': 7777,
|
||||
'pgbouncer_config': None
|
||||
},
|
||||
'node_id': 'nodeB',
|
||||
'is_leader': False,
|
||||
'system_id': None,
|
||||
'timeline_id': None,
|
||||
'status': 'NODE_UNKNOWN',
|
||||
'error': None
|
||||
}
|
||||
}
|
||||
}, mgr.state.export())
|
||||
|
||||
def test_GivenStateFile_WhenCreatingManager_StoredStateIsApplied(self):
|
||||
mgr = Manager(['--config', CONFIG_WITH_STATE_FILE])
|
||||
|
||||
# Create the state file as expected by the loaded config.
|
||||
shutil.copy(STATE_FILE_TEMPLATE, STATE_FILE)
|
||||
|
||||
# Check if the config file was read.
|
||||
# If yes, then the state should be filled with data for the
|
||||
# the configured nodes and all state should be in the initial
|
||||
# configuration.
|
||||
self.maxDiff = 5000
|
||||
self.assertEqual({
|
||||
'system_id': 'A',
|
||||
'timeline_id': 42,
|
||||
'active_pgbouncer_config': None, # because /myt/config.ini from the state does not exit
|
||||
'leader_error': None,
|
||||
'leader_node_id': 'nodeA',
|
||||
'leader_status': 'LEADER_UNKNOWN',
|
||||
'nodes': {
|
||||
'nodeA': {
|
||||
'config': {
|
||||
'host': '1.2.3.4',
|
||||
'port': 8888,
|
||||
'pgbouncer_config': None
|
||||
},
|
||||
'node_id': 'nodeA',
|
||||
'is_leader': True,
|
||||
'system_id': None,
|
||||
'timeline_id': None,
|
||||
'status': 'NODE_UNKNOWN',
|
||||
'error': None
|
||||
},
|
||||
'nodeB': {
|
||||
'config': {
|
||||
'host': '2.3.4.5',
|
||||
'port': 7777,
|
||||
'pgbouncer_config': None
|
||||
},
|
||||
'node_id': 'nodeB',
|
||||
'is_leader': False,
|
||||
'system_id': None,
|
||||
'timeline_id': None,
|
||||
'status': 'NODE_UNKNOWN',
|
||||
'error': None
|
||||
}
|
||||
}
|
||||
}, mgr.state.export())
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import os
|
||||
import unittest
|
||||
from pgbouncemgr.logger import *
|
||||
from pgbouncemgr.state import *
|
||||
|
||||
|
||||
|
@ -12,7 +13,7 @@ PGBOUNCER_CONFIG = os.path.join(
|
|||
|
||||
class StateTests(unittest.TestCase):
|
||||
def test_GivenFreshState_DefaultsAreSetCorrectly(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
self.assertIsNone(state.system_id)
|
||||
self.assertIsNone(state.timeline_id)
|
||||
self.assertIsNone(state.active_pgbouncer_config)
|
||||
|
@ -23,29 +24,29 @@ class StateTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_GivenFreshState_ModifiedFlagIsNotSet(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
|
||||
self.assertFalse(state.modified)
|
||||
|
||||
def test_GivenFreshState_IsCleanSlate_ReturnsTrue(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
|
||||
self.assertTrue(state.is_clean_slate())
|
||||
|
||||
def test_GivenStateWithSystemIdAssigned_IsCleanSlate_ReturnsFalse(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.system_id = "whatever"
|
||||
|
||||
self.assertFalse(state.is_clean_slate())
|
||||
|
||||
def test_GivenFreshState_SetSystemId_SetsSystemId(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.system_id = "booya"
|
||||
|
||||
self.assertEqual("booya", state.system_id)
|
||||
|
||||
def test_GivenStateWithSystemId_SetSameSystemId_IsOk(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.system_id = "booya"
|
||||
state.modified = False
|
||||
state.system_id = "booya"
|
||||
|
@ -53,7 +54,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_GivenStateWithSystemId_SetDifferentSystemId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.system_id = "booya"
|
||||
state.modified = False
|
||||
|
||||
|
@ -62,20 +63,20 @@ class StateTests(unittest.TestCase):
|
|||
self.assertIn("from=booya, to=baayo", str(context.exception))
|
||||
|
||||
def test_GivenFreshState_SetTimelineId_SetsTimelineId(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.timeline_id = 666
|
||||
|
||||
self.assertEqual(666, state.timeline_id)
|
||||
self.assertTrue(state.modified)
|
||||
|
||||
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
with self.assertRaises(InvalidTimelineId) as context:
|
||||
state.timeline_id = "one hundred"
|
||||
self.assertIn("invalid value=one hundred", str(context.exception))
|
||||
|
||||
def test_GivenStateWithTimelineId_SetLowerTimelineId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.timeline_id = "50"
|
||||
|
||||
with self.assertRaises(TimelineIdLowered) as context:
|
||||
|
@ -83,7 +84,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertIn("(from=50, to=49)", str(context.exception))
|
||||
|
||||
def test_GivenStateWithTimelineId_SetSameTimelineId_IsOk(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.timeline_id = 50
|
||||
state.modified = False
|
||||
|
||||
|
@ -92,7 +93,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_GivenStateWithTimelineId_SetHigherTimelineId_IsOk(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.timeline_id = 50
|
||||
state.modified = False
|
||||
|
||||
|
@ -102,7 +103,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetLeaderStatus_SetsLeaderStatus(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.modified = False
|
||||
|
||||
state.leader_status = LEADER_CONNECTED
|
||||
|
@ -111,7 +112,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetLeaderStatus_ToNonExistentStatus_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.modified = False
|
||||
|
||||
with self.assertRaises(InvalidLeaderStatus) as context:
|
||||
|
@ -119,7 +120,7 @@ class StateTests(unittest.TestCase):
|
|||
self.assertIn("'gosh'", str(context.exception))
|
||||
|
||||
def test_SetLeaderError_SetsLeaderError(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.modified = False
|
||||
|
||||
state.leader_error = "God disappeared in a puff of logic"
|
||||
|
@ -136,14 +137,14 @@ class StateTests(unittest.TestCase):
|
|||
|
||||
class NodeCollectionTests(unittest.TestCase):
|
||||
def test_WithNodeNotYetInState_AddNode_AddsNodeState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
|
||||
self.assertEqual(1, node.node_id)
|
||||
self.assertTrue(state.modified)
|
||||
|
||||
def test_WithNodeAlreadyInState_AddNode_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
state.add_node(123)
|
||||
|
||||
with self.assertRaises(DuplicateNodeAdded) as context:
|
||||
|
@ -151,13 +152,13 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertIn("duplicate node_id=123", str(context.exception))
|
||||
|
||||
def test_WithNodeNotInState_getNode_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
|
||||
with self.assertRaises(UnknownNodeRequested):
|
||||
state.get_node("that does not exist")
|
||||
|
||||
def test_WithNodeInState_getNode_ReturnsNode(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node("that does exist")
|
||||
|
||||
node_from_state = state.get_node("that does exist")
|
||||
|
@ -165,15 +166,15 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertEqual(node, node_from_state)
|
||||
|
||||
def test_WithUnknownNode_SetLeaderNode_RaisesException(self):
|
||||
state1 = State()
|
||||
state2 = State()
|
||||
state1 = State(Logger())
|
||||
state2 = State(Logger())
|
||||
node = state2.add_node(1337)
|
||||
with self.assertRaises(UnknownNodeRequested) as context:
|
||||
state1.promote_node(node)
|
||||
self.assertIn("unknown node_id=1337", str(context.exception))
|
||||
|
||||
def test_WithNodeWithoutSystemId_SetLeaderNode_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.timeline_id = 1
|
||||
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
|
@ -183,7 +184,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertIn("node has no system_id", str(context.exception))
|
||||
|
||||
def test_WithNodeWithoutTimelineId_SetLeaderNode_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.system_id = "system"
|
||||
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
|
@ -193,7 +194,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertIn("node has no timeline_id", str(context.exception))
|
||||
|
||||
def test_WithNodeWithoutPgbouncerConfig_SetLeaderNode_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.system_id = "a7d8a9347df789saghdfs"
|
||||
node.timeline_id = 11111111111
|
||||
|
@ -203,7 +204,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertIn("node has no pgbouncer_config", str(context.exception))
|
||||
|
||||
def test_SetLeaderNode_SetsLeaderNode_WithUnknownLeaderStatus(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
node.system_id = "SuperCluster"
|
||||
|
@ -218,7 +219,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetLeaderNode_ToSameLeader_ResetsLeaderNode_WithUnknownLeaderStatus(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
node.system_id = "1.2.3.4.5.6.7.8.9.10.11"
|
||||
|
@ -234,7 +235,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetLeaderNode_ToNodeWithDifferentSystemId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node1 = state.add_node(1)
|
||||
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
node1.system_id = "systemA"
|
||||
|
@ -249,7 +250,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
node2.promote()
|
||||
|
||||
def test_SetLeaderNode_ToNodeWithLowerTimelineId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node1 = state.add_node(1)
|
||||
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
node1.system_id = "systemX"
|
||||
|
@ -264,7 +265,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
node2.promote()
|
||||
|
||||
def test_SetLeaderNode_ToNodeWithSameOrHigherTimelineId_IsOk(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node1 = state.add_node(1)
|
||||
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
node1.system_id = "systemX"
|
||||
|
@ -299,7 +300,7 @@ class NodeCollectionTests(unittest.TestCase):
|
|||
|
||||
class NodeTests(unittest.TestCase):
|
||||
def test_WithNoneValue_SetSystemId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node("break me")
|
||||
state.modified = False
|
||||
|
||||
|
@ -309,7 +310,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_WithEmptyString_SetSystemId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node("break me")
|
||||
state.modified = False
|
||||
|
||||
|
@ -319,7 +320,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
state.modified = False
|
||||
|
||||
|
@ -329,7 +330,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node("break me")
|
||||
|
||||
with self.assertRaises(InvalidTimelineId) as context:
|
||||
|
@ -337,7 +338,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertIn("invalid value=TARDIS", str(context.exception))
|
||||
|
||||
def test_SetTimelineId_SetsTimelineId_AndNotifiesChangeToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
state.modified = False
|
||||
|
||||
|
@ -347,7 +348,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetStatus_SetsStatus_AndNotifiesChangeToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
state.modified = False
|
||||
|
||||
|
@ -357,7 +358,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_WithInvalidStatus_SetStatus_RaisesException(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
state.modified = False
|
||||
|
||||
|
@ -367,7 +368,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertFalse(state.modified)
|
||||
|
||||
def test_SetError_ToString_SetsError_AndNotifiesChangeToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
state.modified = False
|
||||
|
||||
|
@ -377,7 +378,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_SetError_ToNone_ClearsError_AndNotifiesChangeToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node(1)
|
||||
node.set_error("Mouse lost its ball")
|
||||
state.modified = False
|
||||
|
@ -388,7 +389,7 @@ class NodeTests(unittest.TestCase):
|
|||
self.assertTrue(state.modified)
|
||||
|
||||
def test_WhenNothingChanges_NoChangeIsNotifiedToState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
node = state.add_node("x")
|
||||
node.system_id = "aaaaaaa"
|
||||
node.timeline_id = 55
|
||||
|
@ -408,7 +409,7 @@ class StateExportTests(unittest.TestCase):
|
|||
|
||||
def test_GivenFilledState_Export_ExportsDataForStore(self):
|
||||
self.maxDiff = 5000;
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
|
||||
node1 = state.add_node(1)
|
||||
node1.config.pgbouncer_config = PGBOUNCER_CONFIG
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import json
|
||||
import unittest
|
||||
from tempfile import NamedTemporaryFile
|
||||
from pgbouncemgr.logger import *
|
||||
from pgbouncemgr.state import *
|
||||
from pgbouncemgr.state_store import *
|
||||
|
||||
|
@ -15,7 +16,7 @@ def make_test_file_path(filename):
|
|||
|
||||
class StateStoreTests(unittest.TestCase):
|
||||
def test_GivenNonExistingStateFile_OnLoad_StateStoreDoesNotLoadState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
before = state.export()
|
||||
StateStore("/non-existent/state.json", state)
|
||||
after = state.export()
|
||||
|
@ -23,7 +24,7 @@ class StateStoreTests(unittest.TestCase):
|
|||
self.assertEqual(before, after)
|
||||
|
||||
def test_GivenExistingStateFile_ContainingInvalidJson_OnLoad_StateStoreDoesNotLoadState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
before = state.export()
|
||||
StateStore(make_test_file_path("invalid.json"), state)
|
||||
after = state.export()
|
||||
|
@ -31,14 +32,14 @@ class StateStoreTests(unittest.TestCase):
|
|||
self.assertEqual(before, after)
|
||||
|
||||
def test_GivenExistingStateFile_OnLoad_StateStoreUpdatesState(self):
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
|
||||
# These are the fields as
|
||||
# These are the fields as defined in the state.json file.
|
||||
expected = state.export()
|
||||
expected["system_id"] = "A"
|
||||
expected["timeline_id"] = 42
|
||||
expected["leader_node_id"] = "NODE1"
|
||||
expected["active_pgbouncer_config"] = "/my/config.ini"
|
||||
expected["leader_node_id"] = None # because leader node id 'nodeA' does not exist.
|
||||
expected["active_pgbouncer_config"] = None # because stored /my/config.ini does not exist.
|
||||
|
||||
StateStore(make_test_file_path("state.json"), state)
|
||||
|
||||
|
@ -46,7 +47,7 @@ class StateStoreTests(unittest.TestCase):
|
|||
|
||||
def test_GivenState_OnSave_StateStoreStoresState(self):
|
||||
try:
|
||||
state = State()
|
||||
state = State(Logger())
|
||||
tmpfile = NamedTemporaryFile(delete=False)
|
||||
StateStore(tmpfile.name, state).store()
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
---
|
||||
main:
|
||||
state_file: /make/sure/no/state/file/exists/here
|
||||
|
||||
db_connection_defaults:
|
||||
port: 8888
|
||||
password: Wilmaaaaa!!!
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
main:
|
||||
state_file: "/tmp/pgbouncemgr.state"
|
||||
|
||||
db_connection_defaults:
|
||||
port: 8888
|
||||
password: Wilmaaaaa!!!
|
||||
|
||||
nodes:
|
||||
nodeA:
|
||||
host: 1.2.3.4
|
||||
nodeB:
|
||||
host: 2.3.4.5
|
||||
port: 7777
|
|
@ -2,7 +2,7 @@
|
|||
"active_pgbouncer_config": "/my/config.ini",
|
||||
"system_id": "A",
|
||||
"timeline_id": 42,
|
||||
"leader_node_id": "NODE1",
|
||||
"leader_node_id": "nodeA",
|
||||
"any": "other",
|
||||
"state": "properties",
|
||||
"are": "ignored",
|
||||
|
|
Loading…
Reference in New Issue