Work in progress on state store.

This commit is contained in:
Maurice Makaay 2019-12-05 15:38:49 +01:00
parent 6f00ca34cd
commit e5dbf96cb4
11 changed files with 315 additions and 122 deletions

View File

@ -3,42 +3,62 @@
PostgreSQL cluster and that reconfigures pgbouncer when needed.""" PostgreSQL cluster and that reconfigures pgbouncer when needed."""
from argparse import ArgumentParser from argparse import ArgumentParser
from pgbouncemgr.logger import Logger from pgbouncemgr.logger import Logger, ConsoleLogTarget, SyslogLogTarget
from pgbouncemgr.config import Config from pgbouncemgr.config import Config
from pgbouncemgr.state import State
from pgbouncemgr.state_store import StateStore
DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml" DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml"
DEFAULT_LOG_FACILITY = "LOG_LOCAL1" DEFAULT_LOG_FACILITY = "LOG_LOCAL1"
def main(): class Manager():
"""Starts the pgbouncemgr main application.""" def __init__(self, argv=None):
args = _parse_arguments() args = _parse_arguments(argv)
config = Config(args.config) self.config = Config(args.config)
log = Logger("pgbouncemgr", args.log_facility, args.debug, args.verbose) 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 = ArgumentParser(description="pgbouncemgr")
parser.add_argument( parser.add_argument(
"-v", "--verbose", "-v", "--verbose",
default=False, action="store_true", default=False, action="store_true",
help="enable verbose output (default: disabled)") help="enable verbose console output (default: disabled)")
parser.add_argument( parser.add_argument(
"-d", "--debug", "-d", "--debug",
default=False, action="store_true", default=False, action="store_true",
help="enable debugging output (default: disabled)") help="enable debugging console output (default: disabled)")
parser.add_argument( parser.add_argument(
"-f", "--log-facility", "-f", "--log-facility",
default=DEFAULT_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( parser.add_argument(
"--config", "--config",
default=DEFAULT_CONFIG, default=DEFAULT_CONFIG,
help="config file (default: %s)" % DEFAULT_CONFIG) help="config file (default: %s)" % DEFAULT_CONFIG)
args = parser.parse_args() return parser.parse_args(args)
return args
if __name__ == "__main__": if __name__ == "__main__":
main() Manager(None).start()

View File

@ -7,6 +7,10 @@ class NodeConfig():
self._pgbouncer_config = None self._pgbouncer_config = None
self.host = None self.host = None
self.port = None self.port = None
self.connect_timeout = 1
self.name = 'template1'
self.user = 'pgbouncemgr'
self.password = None
@property @property
def pgbouncer_config(self): def pgbouncer_config(self):
@ -21,6 +25,8 @@ class NodeConfig():
self._pgbouncer_config = path self._pgbouncer_config = path
def export(self): def export(self):
"""Exports the data for the node configuration, that we want
to end up in the state data."""
return { return {
"pgbouncer_config": self.pgbouncer_config, "pgbouncer_config": self.pgbouncer_config,
"host": self.host, "host": self.host,

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# no-pylint: disable=missing-docstring,broad-except,too-many-instance-attributes # no-pylint: disable=missing-docstring,broad-except,too-many-instance-attributes
import os
from pgbouncemgr.node_config import NodeConfig from pgbouncemgr.node_config import NodeConfig
@ -85,6 +86,20 @@ class InvalidLeaderStatus(StateException):
("The leader_status cannot be set to '%s' " + ("The leader_status cannot be set to '%s' " +
"(valid values are: %s)") % (status, ", ".join(LEADER_STATUSES))) "(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): class InvalidNodeStatus(StateException):
"""Raised when the status for a node is set to an invalid value.""" """Raised when the status for a node is set to an invalid value."""
def __init__(self, status): def __init__(self, status):
@ -93,12 +108,23 @@ class InvalidNodeStatus(StateException):
"(valid values are: %s)") % (status, ", ".join(NODE_STATUSES))) "(valid values are: %s)") % (status, ", ".join(NODE_STATUSES)))
class State(): 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.modified = False
self._system_id = None self._system_id = None
self._timeline_id = None self._timeline_id = None
self._active_pgbouncer_config = None self._active_pgbouncer_config = None
self.leader_node_id = None self._leader_node_id = None
self._leader_error = None self._leader_error = None
self._leader_status = LEADER_UNKNOWN self._leader_status = LEADER_UNKNOWN
self.nodes = {} self.nodes = {}
@ -204,6 +230,26 @@ class State():
self._leader_status = status self._leader_status = status
self.modified = True 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 @property
def leader_error(self): def leader_error(self):
return self._leader_error return self._leader_error
@ -223,13 +269,22 @@ class State():
return self._active_pgbouncer_config return self._active_pgbouncer_config
@active_pgbouncer_config.setter @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 """Sets the pgbouncer configuration file that must be activated
in order to connect pgbouncer to the correct backend in order to connect pgbouncer to the correct backend
PostgreSQL server.""" PostgreSQL server.
if self._active_pgbouncer_config == pgbouncer_config: 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 return
self._active_pgbouncer_config = pgbouncer_config self._active_pgbouncer_config = active_pgbouncer_config
self.modified = True self.modified = True
def export(self): def export(self):

View File

@ -6,6 +6,8 @@ from datetime import datetime
from os import rename from os import rename
from os.path import isfile from os.path import isfile
from pgbouncemgr.logger import format_ex from pgbouncemgr.logger import format_ex
from pgbouncemgr.state import \
UnknownLeaderNodeId, ActivePgbouncerConfigDoesNotExist
class StateStoreException(Exception): class StateStoreException(Exception):
@ -44,7 +46,18 @@ class StateStore():
"timeline_id", "timeline_id",
"active_pgbouncer_config", "active_pgbouncer_config",
"leader_node_id"]: "leader_node_id"]:
if key in loaded_state:
try:
setattr(self.state, key, loaded_state[key]) 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): def store(self):
"""Store the current state in the state file. Returns True when """Store the current state in the state file. Returns True when

View File

@ -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

134
tests/test_manager.py Normal file
View File

@ -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())

View File

@ -2,6 +2,7 @@
import os import os
import unittest import unittest
from pgbouncemgr.logger import *
from pgbouncemgr.state import * from pgbouncemgr.state import *
@ -12,7 +13,7 @@ PGBOUNCER_CONFIG = os.path.join(
class StateTests(unittest.TestCase): class StateTests(unittest.TestCase):
def test_GivenFreshState_DefaultsAreSetCorrectly(self): def test_GivenFreshState_DefaultsAreSetCorrectly(self):
state = State() state = State(Logger())
self.assertIsNone(state.system_id) self.assertIsNone(state.system_id)
self.assertIsNone(state.timeline_id) self.assertIsNone(state.timeline_id)
self.assertIsNone(state.active_pgbouncer_config) self.assertIsNone(state.active_pgbouncer_config)
@ -23,29 +24,29 @@ class StateTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_GivenFreshState_ModifiedFlagIsNotSet(self): def test_GivenFreshState_ModifiedFlagIsNotSet(self):
state = State() state = State(Logger())
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_GivenFreshState_IsCleanSlate_ReturnsTrue(self): def test_GivenFreshState_IsCleanSlate_ReturnsTrue(self):
state = State() state = State(Logger())
self.assertTrue(state.is_clean_slate()) self.assertTrue(state.is_clean_slate())
def test_GivenStateWithSystemIdAssigned_IsCleanSlate_ReturnsFalse(self): def test_GivenStateWithSystemIdAssigned_IsCleanSlate_ReturnsFalse(self):
state = State() state = State(Logger())
state.system_id = "whatever" state.system_id = "whatever"
self.assertFalse(state.is_clean_slate()) self.assertFalse(state.is_clean_slate())
def test_GivenFreshState_SetSystemId_SetsSystemId(self): def test_GivenFreshState_SetSystemId_SetsSystemId(self):
state = State() state = State(Logger())
state.system_id = "booya" state.system_id = "booya"
self.assertEqual("booya", state.system_id) self.assertEqual("booya", state.system_id)
def test_GivenStateWithSystemId_SetSameSystemId_IsOk(self): def test_GivenStateWithSystemId_SetSameSystemId_IsOk(self):
state = State() state = State(Logger())
state.system_id = "booya" state.system_id = "booya"
state.modified = False state.modified = False
state.system_id = "booya" state.system_id = "booya"
@ -53,7 +54,7 @@ class StateTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_GivenStateWithSystemId_SetDifferentSystemId_RaisesException(self): def test_GivenStateWithSystemId_SetDifferentSystemId_RaisesException(self):
state = State() state = State(Logger())
state.system_id = "booya" state.system_id = "booya"
state.modified = False state.modified = False
@ -62,20 +63,20 @@ class StateTests(unittest.TestCase):
self.assertIn("from=booya, to=baayo", str(context.exception)) self.assertIn("from=booya, to=baayo", str(context.exception))
def test_GivenFreshState_SetTimelineId_SetsTimelineId(self): def test_GivenFreshState_SetTimelineId_SetsTimelineId(self):
state = State() state = State(Logger())
state.timeline_id = 666 state.timeline_id = 666
self.assertEqual(666, state.timeline_id) self.assertEqual(666, state.timeline_id)
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self): def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
state = State() state = State(Logger())
with self.assertRaises(InvalidTimelineId) as context: with self.assertRaises(InvalidTimelineId) as context:
state.timeline_id = "one hundred" state.timeline_id = "one hundred"
self.assertIn("invalid value=one hundred", str(context.exception)) self.assertIn("invalid value=one hundred", str(context.exception))
def test_GivenStateWithTimelineId_SetLowerTimelineId_RaisesException(self): def test_GivenStateWithTimelineId_SetLowerTimelineId_RaisesException(self):
state = State() state = State(Logger())
state.timeline_id = "50" state.timeline_id = "50"
with self.assertRaises(TimelineIdLowered) as context: with self.assertRaises(TimelineIdLowered) as context:
@ -83,7 +84,7 @@ class StateTests(unittest.TestCase):
self.assertIn("(from=50, to=49)", str(context.exception)) self.assertIn("(from=50, to=49)", str(context.exception))
def test_GivenStateWithTimelineId_SetSameTimelineId_IsOk(self): def test_GivenStateWithTimelineId_SetSameTimelineId_IsOk(self):
state = State() state = State(Logger())
state.timeline_id = 50 state.timeline_id = 50
state.modified = False state.modified = False
@ -92,7 +93,7 @@ class StateTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_GivenStateWithTimelineId_SetHigherTimelineId_IsOk(self): def test_GivenStateWithTimelineId_SetHigherTimelineId_IsOk(self):
state = State() state = State(Logger())
state.timeline_id = 50 state.timeline_id = 50
state.modified = False state.modified = False
@ -102,7 +103,7 @@ class StateTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_SetLeaderStatus_SetsLeaderStatus(self): def test_SetLeaderStatus_SetsLeaderStatus(self):
state = State() state = State(Logger())
state.modified = False state.modified = False
state.leader_status = LEADER_CONNECTED state.leader_status = LEADER_CONNECTED
@ -111,7 +112,7 @@ class StateTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_SetLeaderStatus_ToNonExistentStatus_RaisesException(self): def test_SetLeaderStatus_ToNonExistentStatus_RaisesException(self):
state = State() state = State(Logger())
state.modified = False state.modified = False
with self.assertRaises(InvalidLeaderStatus) as context: with self.assertRaises(InvalidLeaderStatus) as context:
@ -119,7 +120,7 @@ class StateTests(unittest.TestCase):
self.assertIn("'gosh'", str(context.exception)) self.assertIn("'gosh'", str(context.exception))
def test_SetLeaderError_SetsLeaderError(self): def test_SetLeaderError_SetsLeaderError(self):
state = State() state = State(Logger())
state.modified = False state.modified = False
state.leader_error = "God disappeared in a puff of logic" state.leader_error = "God disappeared in a puff of logic"
@ -136,14 +137,14 @@ class StateTests(unittest.TestCase):
class NodeCollectionTests(unittest.TestCase): class NodeCollectionTests(unittest.TestCase):
def test_WithNodeNotYetInState_AddNode_AddsNodeState(self): def test_WithNodeNotYetInState_AddNode_AddsNodeState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
self.assertEqual(1, node.node_id) self.assertEqual(1, node.node_id)
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_WithNodeAlreadyInState_AddNode_RaisesException(self): def test_WithNodeAlreadyInState_AddNode_RaisesException(self):
state = State() state = State(Logger())
state.add_node(123) state.add_node(123)
with self.assertRaises(DuplicateNodeAdded) as context: with self.assertRaises(DuplicateNodeAdded) as context:
@ -151,13 +152,13 @@ class NodeCollectionTests(unittest.TestCase):
self.assertIn("duplicate node_id=123", str(context.exception)) self.assertIn("duplicate node_id=123", str(context.exception))
def test_WithNodeNotInState_getNode_RaisesException(self): def test_WithNodeNotInState_getNode_RaisesException(self):
state = State() state = State(Logger())
with self.assertRaises(UnknownNodeRequested): with self.assertRaises(UnknownNodeRequested):
state.get_node("that does not exist") state.get_node("that does not exist")
def test_WithNodeInState_getNode_ReturnsNode(self): def test_WithNodeInState_getNode_ReturnsNode(self):
state = State() state = State(Logger())
node = state.add_node("that does exist") node = state.add_node("that does exist")
node_from_state = state.get_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) self.assertEqual(node, node_from_state)
def test_WithUnknownNode_SetLeaderNode_RaisesException(self): def test_WithUnknownNode_SetLeaderNode_RaisesException(self):
state1 = State() state1 = State(Logger())
state2 = State() state2 = State(Logger())
node = state2.add_node(1337) node = state2.add_node(1337)
with self.assertRaises(UnknownNodeRequested) as context: with self.assertRaises(UnknownNodeRequested) as context:
state1.promote_node(node) state1.promote_node(node)
self.assertIn("unknown node_id=1337", str(context.exception)) self.assertIn("unknown node_id=1337", str(context.exception))
def test_WithNodeWithoutSystemId_SetLeaderNode_RaisesException(self): def test_WithNodeWithoutSystemId_SetLeaderNode_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.timeline_id = 1 node.timeline_id = 1
node.config.pgbouncer_config = PGBOUNCER_CONFIG node.config.pgbouncer_config = PGBOUNCER_CONFIG
@ -183,7 +184,7 @@ class NodeCollectionTests(unittest.TestCase):
self.assertIn("node has no system_id", str(context.exception)) self.assertIn("node has no system_id", str(context.exception))
def test_WithNodeWithoutTimelineId_SetLeaderNode_RaisesException(self): def test_WithNodeWithoutTimelineId_SetLeaderNode_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.system_id = "system" node.system_id = "system"
node.config.pgbouncer_config = PGBOUNCER_CONFIG node.config.pgbouncer_config = PGBOUNCER_CONFIG
@ -193,7 +194,7 @@ class NodeCollectionTests(unittest.TestCase):
self.assertIn("node has no timeline_id", str(context.exception)) self.assertIn("node has no timeline_id", str(context.exception))
def test_WithNodeWithoutPgbouncerConfig_SetLeaderNode_RaisesException(self): def test_WithNodeWithoutPgbouncerConfig_SetLeaderNode_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.system_id = "a7d8a9347df789saghdfs" node.system_id = "a7d8a9347df789saghdfs"
node.timeline_id = 11111111111 node.timeline_id = 11111111111
@ -203,7 +204,7 @@ class NodeCollectionTests(unittest.TestCase):
self.assertIn("node has no pgbouncer_config", str(context.exception)) self.assertIn("node has no pgbouncer_config", str(context.exception))
def test_SetLeaderNode_SetsLeaderNode_WithUnknownLeaderStatus(self): def test_SetLeaderNode_SetsLeaderNode_WithUnknownLeaderStatus(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.config.pgbouncer_config = PGBOUNCER_CONFIG node.config.pgbouncer_config = PGBOUNCER_CONFIG
node.system_id = "SuperCluster" node.system_id = "SuperCluster"
@ -218,7 +219,7 @@ class NodeCollectionTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_SetLeaderNode_ToSameLeader_ResetsLeaderNode_WithUnknownLeaderStatus(self): def test_SetLeaderNode_ToSameLeader_ResetsLeaderNode_WithUnknownLeaderStatus(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.config.pgbouncer_config = PGBOUNCER_CONFIG node.config.pgbouncer_config = PGBOUNCER_CONFIG
node.system_id = "1.2.3.4.5.6.7.8.9.10.11" 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) self.assertTrue(state.modified)
def test_SetLeaderNode_ToNodeWithDifferentSystemId_RaisesException(self): def test_SetLeaderNode_ToNodeWithDifferentSystemId_RaisesException(self):
state = State() state = State(Logger())
node1 = state.add_node(1) node1 = state.add_node(1)
node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.config.pgbouncer_config = PGBOUNCER_CONFIG
node1.system_id = "systemA" node1.system_id = "systemA"
@ -249,7 +250,7 @@ class NodeCollectionTests(unittest.TestCase):
node2.promote() node2.promote()
def test_SetLeaderNode_ToNodeWithLowerTimelineId_RaisesException(self): def test_SetLeaderNode_ToNodeWithLowerTimelineId_RaisesException(self):
state = State() state = State(Logger())
node1 = state.add_node(1) node1 = state.add_node(1)
node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.config.pgbouncer_config = PGBOUNCER_CONFIG
node1.system_id = "systemX" node1.system_id = "systemX"
@ -264,7 +265,7 @@ class NodeCollectionTests(unittest.TestCase):
node2.promote() node2.promote()
def test_SetLeaderNode_ToNodeWithSameOrHigherTimelineId_IsOk(self): def test_SetLeaderNode_ToNodeWithSameOrHigherTimelineId_IsOk(self):
state = State() state = State(Logger())
node1 = state.add_node(1) node1 = state.add_node(1)
node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.config.pgbouncer_config = PGBOUNCER_CONFIG
node1.system_id = "systemX" node1.system_id = "systemX"
@ -299,7 +300,7 @@ class NodeCollectionTests(unittest.TestCase):
class NodeTests(unittest.TestCase): class NodeTests(unittest.TestCase):
def test_WithNoneValue_SetSystemId_RaisesException(self): def test_WithNoneValue_SetSystemId_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node("break me") node = state.add_node("break me")
state.modified = False state.modified = False
@ -309,7 +310,7 @@ class NodeTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_WithEmptyString_SetSystemId_RaisesException(self): def test_WithEmptyString_SetSystemId_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node("break me") node = state.add_node("break me")
state.modified = False state.modified = False
@ -319,7 +320,7 @@ class NodeTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self): def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
state.modified = False state.modified = False
@ -329,7 +330,7 @@ class NodeTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_WithNonIntegerInput_SetTimelineId_RaisesException(self): def test_WithNonIntegerInput_SetTimelineId_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node("break me") node = state.add_node("break me")
with self.assertRaises(InvalidTimelineId) as context: with self.assertRaises(InvalidTimelineId) as context:
@ -337,7 +338,7 @@ class NodeTests(unittest.TestCase):
self.assertIn("invalid value=TARDIS", str(context.exception)) self.assertIn("invalid value=TARDIS", str(context.exception))
def test_SetTimelineId_SetsTimelineId_AndNotifiesChangeToState(self): def test_SetTimelineId_SetsTimelineId_AndNotifiesChangeToState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
state.modified = False state.modified = False
@ -347,7 +348,7 @@ class NodeTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_SetStatus_SetsStatus_AndNotifiesChangeToState(self): def test_SetStatus_SetsStatus_AndNotifiesChangeToState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
state.modified = False state.modified = False
@ -357,7 +358,7 @@ class NodeTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_WithInvalidStatus_SetStatus_RaisesException(self): def test_WithInvalidStatus_SetStatus_RaisesException(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
state.modified = False state.modified = False
@ -367,7 +368,7 @@ class NodeTests(unittest.TestCase):
self.assertFalse(state.modified) self.assertFalse(state.modified)
def test_SetError_ToString_SetsError_AndNotifiesChangeToState(self): def test_SetError_ToString_SetsError_AndNotifiesChangeToState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
state.modified = False state.modified = False
@ -377,7 +378,7 @@ class NodeTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_SetError_ToNone_ClearsError_AndNotifiesChangeToState(self): def test_SetError_ToNone_ClearsError_AndNotifiesChangeToState(self):
state = State() state = State(Logger())
node = state.add_node(1) node = state.add_node(1)
node.set_error("Mouse lost its ball") node.set_error("Mouse lost its ball")
state.modified = False state.modified = False
@ -388,7 +389,7 @@ class NodeTests(unittest.TestCase):
self.assertTrue(state.modified) self.assertTrue(state.modified)
def test_WhenNothingChanges_NoChangeIsNotifiedToState(self): def test_WhenNothingChanges_NoChangeIsNotifiedToState(self):
state = State() state = State(Logger())
node = state.add_node("x") node = state.add_node("x")
node.system_id = "aaaaaaa" node.system_id = "aaaaaaa"
node.timeline_id = 55 node.timeline_id = 55
@ -408,7 +409,7 @@ class StateExportTests(unittest.TestCase):
def test_GivenFilledState_Export_ExportsDataForStore(self): def test_GivenFilledState_Export_ExportsDataForStore(self):
self.maxDiff = 5000; self.maxDiff = 5000;
state = State() state = State(Logger())
node1 = state.add_node(1) node1 = state.add_node(1)
node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.config.pgbouncer_config = PGBOUNCER_CONFIG

View File

@ -4,6 +4,7 @@ import os
import json import json
import unittest import unittest
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from pgbouncemgr.logger import *
from pgbouncemgr.state import * from pgbouncemgr.state import *
from pgbouncemgr.state_store import * from pgbouncemgr.state_store import *
@ -15,7 +16,7 @@ def make_test_file_path(filename):
class StateStoreTests(unittest.TestCase): class StateStoreTests(unittest.TestCase):
def test_GivenNonExistingStateFile_OnLoad_StateStoreDoesNotLoadState(self): def test_GivenNonExistingStateFile_OnLoad_StateStoreDoesNotLoadState(self):
state = State() state = State(Logger())
before = state.export() before = state.export()
StateStore("/non-existent/state.json", state) StateStore("/non-existent/state.json", state)
after = state.export() after = state.export()
@ -23,7 +24,7 @@ class StateStoreTests(unittest.TestCase):
self.assertEqual(before, after) self.assertEqual(before, after)
def test_GivenExistingStateFile_ContainingInvalidJson_OnLoad_StateStoreDoesNotLoadState(self): def test_GivenExistingStateFile_ContainingInvalidJson_OnLoad_StateStoreDoesNotLoadState(self):
state = State() state = State(Logger())
before = state.export() before = state.export()
StateStore(make_test_file_path("invalid.json"), state) StateStore(make_test_file_path("invalid.json"), state)
after = state.export() after = state.export()
@ -31,14 +32,14 @@ class StateStoreTests(unittest.TestCase):
self.assertEqual(before, after) self.assertEqual(before, after)
def test_GivenExistingStateFile_OnLoad_StateStoreUpdatesState(self): 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 = state.export()
expected["system_id"] = "A" expected["system_id"] = "A"
expected["timeline_id"] = 42 expected["timeline_id"] = 42
expected["leader_node_id"] = "NODE1" expected["leader_node_id"] = None # because leader node id 'nodeA' does not exist.
expected["active_pgbouncer_config"] = "/my/config.ini" expected["active_pgbouncer_config"] = None # because stored /my/config.ini does not exist.
StateStore(make_test_file_path("state.json"), state) StateStore(make_test_file_path("state.json"), state)
@ -46,7 +47,7 @@ class StateStoreTests(unittest.TestCase):
def test_GivenState_OnSave_StateStoreStoresState(self): def test_GivenState_OnSave_StateStoreStoresState(self):
try: try:
state = State() state = State(Logger())
tmpfile = NamedTemporaryFile(delete=False) tmpfile = NamedTemporaryFile(delete=False)
StateStore(tmpfile.name, state).store() StateStore(tmpfile.name, state).store()

View File

@ -1,4 +1,7 @@
--- ---
main:
state_file: /make/sure/no/state/file/exists/here
db_connection_defaults: db_connection_defaults:
port: 8888 port: 8888
password: Wilmaaaaa!!! password: Wilmaaaaa!!!

View File

@ -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

View File

@ -2,7 +2,7 @@
"active_pgbouncer_config": "/my/config.ini", "active_pgbouncer_config": "/my/config.ini",
"system_id": "A", "system_id": "A",
"timeline_id": 42, "timeline_id": 42,
"leader_node_id": "NODE1", "leader_node_id": "nodeA",
"any": "other", "any": "other",
"state": "properties", "state": "properties",
"are": "ignored", "are": "ignored",