Moved all constants into their own pgbouncemgr.constants module.

This commit is contained in:
Maurice Makaay 2019-12-13 14:17:15 +01:00
parent a3a97a9721
commit 4f8f1473a1
10 changed files with 107 additions and 47 deletions

View File

@ -13,10 +13,13 @@ run:
PYTHONPATH=./ python3 pgbouncemgr/manager.py PYTHONPATH=./ python3 pgbouncemgr/manager.py
run-d: run-d:
PYTHONPATH=./ python3 pgbouncemgr/manager.py -d PYTHONPATH=./ python3 pgbouncemgr/manager.py --debug
run-s:
PYTHONPATH=./ python3 pgbouncemgr/manager.py --debug --single-shot
run-v: run-v:
PYTHONPATH=./ python3 pgbouncemgr/manager.py -v PYTHONPATH=./ python3 pgbouncemgr/manager.py --verbose
run-h: run-h:
PYTHONPATH=./ python3 pgbouncemgr/manager.py -h PYTHONPATH=./ python3 pgbouncemgr/manager.py -h

View File

@ -55,7 +55,7 @@ class Config():
"connect_timeout": 1, "connect_timeout": 1,
"user": "pgbouncemgr", "user": "pgbouncemgr",
"password": None, "password": None,
"name": "template1", "database": "template1",
} }
self.pgbouncer = { self.pgbouncer = {
"pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini", "pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini",

25
pgbouncemgr/constants.py Normal file
View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""Constants as used by the pgbouncemgr modules."""
# Configuration defaults.
DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml"
DEFAULT_LOG_FACILITY = "LOG_LOCAL1"
# Return values for the PgConnection.connect() method.
CONN_CONNECTED = 'CONNECTED'
CONN_REUSED = 'REUSED'
CONN_RECONNECTED = 'RECONNECTED'
# Backend node status.
NODE_UNKNOWN = "NODE_UNKNOWN"
NODE_OFFLINE = "NODE_OFFLINE"
NODE_PRIMARY = "NODE_PRIMARY"
NODE_STANDBY = "NODE_STANDBY"
NODE_STATUSES = [NODE_UNKNOWN, NODE_OFFLINE, NODE_PRIMARY, NODE_STANDBY]
# Leader node status.
LEADER_UNKNOWN = "LEADER_UNKNOWN"
LEADER_CONNECTED = "LEADER_CONNECTED"
LEADER_DISCONNECTED = "LEADER_DISCONNECTED"
LEADER_STATUSES = [LEADER_UNKNOWN, LEADER_CONNECTED, LEADER_DISCONNECTED]

View File

@ -6,6 +6,7 @@
from time import sleep from time import sleep
from argparse import ArgumentParser from argparse import ArgumentParser
from pgbouncemgr.constants import DEFAULT_CONFIG, DEFAULT_LOG_FACILITY
from pgbouncemgr.logger import Logger, ConsoleLog, SyslogLog from pgbouncemgr.logger import Logger, ConsoleLog, SyslogLog
from pgbouncemgr.config import Config from pgbouncemgr.config import Config
from pgbouncemgr.state import State from pgbouncemgr.state import State
@ -14,14 +15,11 @@ from pgbouncemgr.state_store import StateStore
from pgbouncemgr.node_poller import NodePoller from pgbouncemgr.node_poller import NodePoller
DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml"
DEFAULT_LOG_FACILITY = "LOG_LOCAL1"
class Manager(): class Manager():
def __init__(self, argv=None): def __init__(self, argv=None):
args = _parse_arguments(argv) args = _parse_arguments(argv)
self.config = Config(args.config) self.config = Config(args.config)
self.single_shot = args.single_shot
self._create_logger(args) self._create_logger(args)
self._create_state() self._create_state()
self.node_poller = NodePoller(self.state) self.node_poller = NodePoller(self.state)
@ -42,6 +40,8 @@ class Manager():
drop_privileges(self.config.run_user, self.config.run_group) drop_privileges(self.config.run_user, self.config.run_group)
while True: while True:
self.node_poller.poll() self.node_poller.poll()
if self.single_shot:
return
sleep(self.config.poll_interval_in_sec) sleep(self.config.poll_interval_in_sec)
@ -55,6 +55,11 @@ def _parse_arguments(args):
"-d", "--debug", "-d", "--debug",
default=False, action="store_true", default=False, action="store_true",
help="enable debugging console output (default: disabled)") help="enable debugging console output (default: disabled)")
parser.add_argument(
"-s", "--single-shot",
default=False, action="store_true",
help="do only a single run, instead of running continuously " +
"(default: disabled)")
parser.add_argument( parser.add_argument(
"-f", "--log-facility", "-f", "--log-facility",
default=DEFAULT_LOG_FACILITY, default=DEFAULT_LOG_FACILITY,

View File

@ -14,7 +14,7 @@ class NodeConfig():
self.host = None self.host = None
self.port = None self.port = None
self.connect_timeout = 1 self.connect_timeout = 1
self.name = 'template1' self.database = 'template1'
self.user = 'pgbouncemgr' self.user = 'pgbouncemgr'
self.password = None self.password = None

View File

@ -1,13 +1,47 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# no-pylint: disable=missing-docstring,too-few-public-methods # no-pylint: disable=missing-docstring,too-few-public-methods
from pgbouncemgr.postgres import PgReplicationConnection, PgException
class NodePoller(): class NodePoller():
"""The NodePoller is used to poll all the nodes that are available """The NodePoller is used to poll all the nodes that are available
in the state object, and to update their status according to in the state object, and to update their status according to
the results.""" the results.
def __init__(self, state): The connection_class that can be provided is used for testing
purposes (dependency injection)."""
def __init__(self, state, connection_class=None):
self.connection_class = PgReplicationConnection
if connection_class is not None:
self.connection_class = connection_class
self.state = state self.state = state
self._connections = {}
def poll(self): def poll(self):
"""Check the status of all backend nodes and update their status."""
for node in self.state.nodes.values(): for node in self.state.nodes.values():
print(repr(node.config)) self._poll_node(node)
def _poll_node(self, node):
connection = self._get_connection_object(node)
try:
result = connection.connect()
print(result)
status = connection.get_replication_status()
node.status = status["status"]
node.timeline_id = status["timeline_id"]
node.system_id = status["system_id"]
except PgException as exception:
pass
def _get_connection_object(self, node):
if node.node_id in self._connections:
return self._connections[node.node_id]
connection = self.connection_class(node.config)
self._connections[node.node_id] = connection
return connection
def reset(self):
"""Reset all database connections."""
for connection in self._connections:
connection.disconnect()
self._connections = {}

View File

@ -8,6 +8,9 @@ import multiprocessing
import psycopg2 import psycopg2
from psycopg2.extras import LogicalReplicationConnection from psycopg2.extras import LogicalReplicationConnection
from pgbouncemgr.logger import format_ex from pgbouncemgr.logger import format_ex
from pgbouncemgr.constants import \
NODE_OFFLINE, NODE_STANDBY, NODE_PRIMARY,\
CONN_REUSED, CONN_RECONNECTED, CONN_CONNECTED
class PgException(Exception): class PgException(Exception):
@ -34,14 +37,16 @@ class ConnectedToWrongBackend(PgException):
"backend service: %s" % msg) "backend service: %s" % msg)
# Return values for the PgConnection.connect() method.
CONNECTED = 'CONNECTED'
REUSED = 'REUSED'
RECONNECTED = 'RECONNECTED'
# The properties that can be used in a configuration object to # The properties that can be used in a configuration object to
# define connection parameters for psycopg2. # define connection parameters for psycopg2.
CONNECTION_PARAMS = ['host', 'port', 'connect_timeout', 'user', 'password'] CONNECTION_PARAMS = [
'host',
'port',
'user',
'password',
'database',
'connect_timeout'
]
class PgConnection(): class PgConnection():
"""Implements a connection to a PostgreSQL server.""" """Implements a connection to a PostgreSQL server."""
@ -69,20 +74,20 @@ class PgConnection():
this connection. If no, or when no connection exists, then this connection. If no, or when no connection exists, then
setup a new connection. setup a new connection.
Raises an exeption when the database connection cannot be setup. Raises an exeption when the database connection cannot be setup.
returns CONNECTED, REUSED or RECONNECTED when the connection returns CONN_CONNECTED, CONN_REUSED or CONN_RECONNECTED when
was setup successfully.""" the connection was setup successfully."""
reconnected = False reconnected = False
if self.conn is not None: if self.conn is not None:
try: try:
with self.conn.cursor() as cursor: with self.conn.cursor() as cursor:
cursor.execute(self.ping_query) cursor.execute(self.ping_query)
return REUSED return CONN_REUSED
except psycopg2.OperationalError: except psycopg2.OperationalError:
reconnected = True reconnected = True
self.disconnect() self.disconnect()
try: try:
self.conn = psycopg2.connect(**self.conn_params) self.conn = psycopg2.connect(**self.conn_params)
return RECONNECTED if reconnected else CONNECTED return CONN_RECONNECTED if reconnected else CONN_CONNECTED
except psycopg2.OperationalError as exception: except psycopg2.OperationalError as exception:
self.disconnect() self.disconnect()
raise PgConnectionFailed(exception) raise PgConnectionFailed(exception)
@ -97,11 +102,6 @@ class PgConnection():
self.conn = None self.conn = None
# Return values for the PgReplicationConnection status.
OFFLINE = "OFFLINE"
PRIMARY = "PRIMARY"
STANDBY = "STANDBY"
class PgReplicationConnection(PgConnection): class PgReplicationConnection(PgConnection):
"""This PostgresQL connection class is used to setup a replication """This PostgresQL connection class is used to setup a replication
connection to a PostgreSQL database server, which can be used connection to a PostgreSQL database server, which can be used
@ -112,19 +112,20 @@ class PgReplicationConnection(PgConnection):
def get_replication_status(self): def get_replication_status(self):
"""Returns the replication status for a node. This is an array, """Returns the replication status for a node. This is an array,
containing the keys "status" (OFFLINE, PRIMARY or STANDBY), containing the keys "status" (NODE_OFFLINE, NODE_PRIMARY or
"system_id" and "timeline_id".""" NODE_STANDBY), "system_id" and "timeline_id"."""
status = { status = {
"status": None, "status": None,
"system_id": None, "system_id": None,
"timeline_id": None "timeline_id": None
} }
# Try to connect to the node. If this fails, the node is OFFLINE. # Try to connect to the node. If this fails, the status is
# set to NODE_OFFLINE.
try: try:
self.connect() self.connect()
except PgConnectionFailed: except PgConnectionFailed:
status["status"] = OFFLINE status["status"] = NODE_OFFLINE
return status return status
# Check if the node is running in primary or standby mode. # Check if the node is running in primary or standby mode.
@ -132,7 +133,7 @@ class PgReplicationConnection(PgConnection):
with self.conn.cursor() as cursor: with self.conn.cursor() as cursor:
cursor.execute("SELECT pg_is_in_recovery()") cursor.execute("SELECT pg_is_in_recovery()")
in_recovery = cursor.fetchone()[0] in_recovery = cursor.fetchone()[0]
status["status"] = STANDBY if in_recovery else PRIMARY status["status"] = NODE_STANDBY if in_recovery else NODE_PRIMARY
except psycopg2.InternalError as exception: except psycopg2.InternalError as exception:
self.disconnect() self.disconnect()
raise RetrievingPgReplicationStatusFailed( raise RetrievingPgReplicationStatusFailed(

View File

@ -3,18 +3,9 @@
import os import os
from pgbouncemgr.node_config import NodeConfig from pgbouncemgr.node_config import NodeConfig
from pgbouncemgr.constants import \
LEADER_UNKNOWN, LEADER_STATUSES,\
LEADER_UNKNOWN = "LEADER_UNKNOWN" NODE_UNKNOWN, NODE_OFFLINE, NODE_PRIMARY, NODE_STANDBY, NODE_STATUSES
LEADER_CONNECTED = "LEADER_CONNECTED"
LEADER_DISCONNECTED = "LEADER_DISCONNECTED"
LEADER_STATUSES = [LEADER_UNKNOWN, LEADER_CONNECTED, LEADER_DISCONNECTED]
NODE_UNKNOWN = "NODE_UNKNOWN"
NODE_OFFLINE = "NODE_OFFLINE"
NODE_PRIMARY = "NODE_PRIMARY"
NODE_STANDBY = "NODE_STANDBY"
NODE_STATUSES = [NODE_UNKNOWN, NODE_OFFLINE, NODE_PRIMARY, NODE_STANDBY]
class StateException(Exception): class StateException(Exception):

View File

@ -52,7 +52,7 @@ class ConfigTests(unittest.TestCase):
self.assertEqual(1, config.db_connection_defaults["connect_timeout"]) self.assertEqual(1, config.db_connection_defaults["connect_timeout"])
self.assertEqual("pgbouncemgr", config.db_connection_defaults["user"]) self.assertEqual("pgbouncemgr", config.db_connection_defaults["user"])
self.assertEqual(None, config.db_connection_defaults["password"]) self.assertEqual(None, config.db_connection_defaults["password"])
self.assertEqual("template1", config.db_connection_defaults["name"]) self.assertEqual("template1", config.db_connection_defaults["database"])
# pgbouncer # pgbouncer
self.assertEqual("/etc/pgbouncer/pgbouncer.ini", config.pgbouncer["pgbouncer_config"]) self.assertEqual("/etc/pgbouncer/pgbouncer.ini", config.pgbouncer["pgbouncer_config"])
self.assertEqual(6432, config.pgbouncer["port"]) self.assertEqual(6432, config.pgbouncer["port"])
@ -64,7 +64,7 @@ class ConfigTests(unittest.TestCase):
self.assertEqual({ self.assertEqual({
"connect_timeout": 1, "connect_timeout": 1,
"host": "0.0.0.0", "host": "0.0.0.0",
"name": "template1", "database": "template1",
"password": "Wilmaaaaa!!!", "password": "Wilmaaaaa!!!",
"pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini", "pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini",
"port": 6432, "port": 6432,
@ -74,7 +74,7 @@ class ConfigTests(unittest.TestCase):
"nodeA": { "nodeA": {
"connect_timeout": 1, "connect_timeout": 1,
"host": "1.2.3.4", "host": "1.2.3.4",
"name": "template1", "database": "template1",
"password": "Wilmaaaaa!!!", "password": "Wilmaaaaa!!!",
"port": 8888, "port": 8888,
"user": "pgbouncemgr" "user": "pgbouncemgr"
@ -82,7 +82,7 @@ class ConfigTests(unittest.TestCase):
"nodeB": { "nodeB": {
"connect_timeout": 1, "connect_timeout": 1,
"host": "2.3.4.5", "host": "2.3.4.5",
"name": "template1", "database": "template1",
"password": "Wilmaaaaa!!!", "password": "Wilmaaaaa!!!",
"port": 7777, "port": 7777,
"user": "pgbouncemgr" "user": "pgbouncemgr"

View File

@ -4,6 +4,7 @@ import os
import unittest import unittest
from pgbouncemgr.logger import * from pgbouncemgr.logger import *
from pgbouncemgr.state import * from pgbouncemgr.state import *
from pgbouncemgr.constants import LEADER_UNKNOWN, LEADER_CONNECTED
PGBOUNCER_CONFIG = os.path.join( PGBOUNCER_CONFIG = os.path.join(