Skip to content

PortManager #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
09dab31
PostgresNode refactoring [PostgresNodePortManager and RO-properties]
dmitry-lipetsk Apr 3, 2025
ef095d3
[FIX] clone_with_new_name_and_base_dir did not respect port_manager
dmitry-lipetsk Apr 4, 2025
0f842fd
PostgresNodePortManager__ThisHost is defined (was: PostgresNodePortMa…
dmitry-lipetsk Apr 4, 2025
e1e609e
PostgresNodePortManager__Generic is added
dmitry-lipetsk Apr 4, 2025
285e5b7
PostgresNodePortManager is added in public API
dmitry-lipetsk Apr 4, 2025
a974866
Test structures were refactored (local, local2, remote)
dmitry-lipetsk Apr 4, 2025
110947d
CI files are updated
dmitry-lipetsk Apr 4, 2025
4f49dde
TestTestgresCommon.test_pgbench is added
dmitry-lipetsk Apr 4, 2025
4fbf51d
PostgresNodePortManager is updated [error messages]
dmitry-lipetsk Apr 4, 2025
17c73cb
PostgresNodePortManager(+company) was moved in own file - port_manage…
dmitry-lipetsk Apr 4, 2025
b1cee19
PortManager was deleted [amen]
dmitry-lipetsk Apr 4, 2025
c5ad907
PostgresNodePortManager was renamed with PortManager
dmitry-lipetsk Apr 4, 2025
0967057
TestTestgresCommon.test_unix_sockets is added
dmitry-lipetsk Apr 4, 2025
1d450b2
TestTestgresCommon.test_the_same_port is added
dmitry-lipetsk Apr 4, 2025
4a38b35
[TestTestgresCommon] New tests are added
dmitry-lipetsk Apr 4, 2025
322fb23
RemoteOperations::is_port_free is updated
dmitry-lipetsk Apr 4, 2025
94da63e
Tests for OsOps::is_port_free are added
dmitry-lipetsk Apr 4, 2025
88f9b73
TestTestgresCommon is corrected [python problems]
dmitry-lipetsk Apr 4, 2025
d9558ce
The call of RaiseError.CommandExecutionError is fixed [message, not m…
dmitry-lipetsk Apr 4, 2025
0da4c21
[CI] ubuntu 24.04 does not have nc
dmitry-lipetsk Apr 4, 2025
0058508
RemoteOperations is update [private method names]
dmitry-lipetsk Apr 5, 2025
8f3a566
test_is_port_free__true is updated
dmitry-lipetsk Apr 5, 2025
d8ebdb7
RemoteOperations::is_port_free is updated (comments)
dmitry-lipetsk Apr 5, 2025
30e472c
setup.py is updated [testgres.helpers was deleted]
dmitry-lipetsk Apr 5, 2025
c94bbb5
Comment in node.py is updated
dmitry-lipetsk Apr 5, 2025
04f88c7
PostgresNode::_node was deleted [use self._os_ops.host]
dmitry-lipetsk Apr 5, 2025
0a3442a
PostgresNode::start is corrected [error message]
dmitry-lipetsk Apr 5, 2025
30124f3
Merge branch 'master' into D20250403_001--port_manager
dmitry-lipetsk Apr 5, 2025
13e71d8
[FIX] PostgresNode.__init__ must not test "os_ops.host" attribute.
dmitry-lipetsk Apr 5, 2025
9e14f4a
PostgresNode.free_port always set a port to None
dmitry-lipetsk Apr 6, 2025
739ef61
[FIX] flake8 (noqa: E721)
dmitry-lipetsk Apr 6, 2025
696cc1e
PortManager__Generic is refactored
dmitry-lipetsk Apr 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
PostgresNodePortManager__Generic is added
  • Loading branch information
dmitry-lipetsk committed Apr 4, 2025
commit e1e609e6dd5fe705c6c63c9bc6f3fb045992c268
55 changes: 53 additions & 2 deletions testgres/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@
options_string, \
clean_on_error

from .helpers.port_manager import PortForException

from .backup import NodeBackup

from .operations.os_ops import ConnectionParams
Expand Down Expand Up @@ -172,6 +174,52 @@ def release_port(self, number: int) -> None:
return utils.release_port(number)


class PostgresNodePortManager__Generic(PostgresNodePortManager):
_os_ops: OsOperations
_allocated_ports_guard: object
_allocated_ports: set[int]

def __init__(self, os_ops: OsOperations):
assert os_ops is not None
assert isinstance(os_ops, OsOperations)
self._os_ops = os_ops
self._allocated_ports_guard = threading.Lock()
self._allocated_ports = set[int]()

def reserve_port(self) -> int:
ports = set(range(1024, 65535))
assert type(ports) == set # noqa: E721

assert self._allocated_ports_guard is not None
assert type(self._allocated_ports) == set # noqa: E721

with self._allocated_ports_guard:
ports.difference_update(self._allocated_ports)

sampled_ports = random.sample(tuple(ports), min(len(ports), 100))

for port in sampled_ports:
assert not (port in self._allocated_ports)

if not self._os_ops.is_port_free(port):
continue

self._allocated_ports.add(port)
return port

raise PortForException("Can't select a port")

def release_port(self, number: int) -> None:
assert type(number) == int # noqa: E721

assert self._allocated_ports_guard is not None
assert type(self._allocated_ports) == set # noqa: E721

with self._allocated_ports_guard:
assert number in self._allocated_ports
self._allocated_ports.discard(number)


class PostgresNode(object):
# a max number of node start attempts
_C_MAX_START_ATEMPTS = 5
Expand Down Expand Up @@ -308,8 +356,11 @@ def _get_port_manager(os_ops: OsOperations) -> PostgresNodePortManager:
assert os_ops is not None
assert isinstance(os_ops, OsOperations)

# [2025-04-03] It is our old, incorrected behaviour
return PostgresNodePortManager__ThisHost()
if isinstance(os_ops, LocalOperations):
return PostgresNodePortManager__ThisHost()

# TODO: Throw exception "Please define a port manager."
return PostgresNodePortManager__Generic(os_ops)

def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
assert name is None or type(name) == str # noqa: E721
Expand Down
11 changes: 11 additions & 0 deletions testgres/operations/local_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import tempfile
import time
import socket

import psutil

Expand Down Expand Up @@ -436,6 +437,16 @@ def get_process_children(self, pid):
assert type(pid) == int # noqa: E721
return psutil.Process(pid).children()

def is_port_free(self, number: int) -> bool:
assert type(number) == int # noqa: E721

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("", number))
return True
except OSError:
return False

# Database control
def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
conn = pglib.connect(
Expand Down
4 changes: 4 additions & 0 deletions testgres/operations/os_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def get_pid(self):
def get_process_children(self, pid):
raise NotImplementedError()

def is_port_free(self, number: int):
assert type(number) == int # noqa: E721
raise NotImplementedError()

# Database control
def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
raise NotImplementedError()
38 changes: 38 additions & 0 deletions testgres/operations/remote_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,44 @@ def get_process_children(self, pid):

raise ExecUtilException(f"Error in getting process children. Error: {result.stderr}")

def is_port_free(self, number: int) -> bool:
assert type(number) == int # noqa: E721

cmd = ["nc", "-w", "5", "-z", "-v", "localhost", str(number)]

exit_status, output, error = self.exec_command(cmd=cmd, encoding=get_default_encoding(), ignore_errors=True, verbose=True)

assert type(output) == str # noqa: E721
assert type(error) == str # noqa: E721

if exit_status == 0:
return __class__.helper__is_port_free__process_0(output)

if exit_status == 1:
return __class__.helper__is_port_free__process_1(error)

errMsg = "nc returns an unknown result code: {0}".format(exit_status)

RaiseError.CommandExecutionError(
cmd=cmd,
exit_code=exit_status,
msg_arg=errMsg,
error=error,
out=output
)

@staticmethod
def helper__is_port_free__process_0(output: str) -> bool:
assert type(output) == str # noqa: E721
# TODO: check output message
return False

@staticmethod
def helper__is_port_free__process_1(error: str) -> bool:
assert type(error) == str # noqa: E721
# TODO: check error message
return True

# Database control
def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
conn = pglib.connect(
Expand Down
67 changes: 0 additions & 67 deletions tests/test_testgres_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import subprocess

import pytest
import psutil
import logging

from .helpers.os_ops_descrs import OsOpsDescrs
Expand All @@ -27,8 +26,6 @@
get_pg_config

# NOTE: those are ugly imports
from ..testgres import bound_ports
from ..testgres.node import ProcessProxy


def util_exists(util):
Expand Down Expand Up @@ -259,70 +256,6 @@ def test_unix_sockets(self):
assert (res_exec == [(1,)])
assert (res_psql == b'1\n')

def test_ports_management(self):
assert bound_ports is not None
assert type(bound_ports) == set # noqa: E721

if len(bound_ports) != 0:
logging.warning("bound_ports is not empty: {0}".format(bound_ports))

stage0__bound_ports = bound_ports.copy()

with __class__.helper__get_node() as node:
assert bound_ports is not None
assert type(bound_ports) == set # noqa: E721

assert node.port is not None
assert type(node.port) == int # noqa: E721

logging.info("node port is {0}".format(node.port))

assert node.port in bound_ports
assert node.port not in stage0__bound_ports

assert stage0__bound_ports <= bound_ports
assert len(stage0__bound_ports) + 1 == len(bound_ports)

stage1__bound_ports = stage0__bound_ports.copy()
stage1__bound_ports.add(node.port)

assert stage1__bound_ports == bound_ports

# check that port has been freed successfully
assert bound_ports is not None
assert type(bound_ports) == set # noqa: E721
assert bound_ports == stage0__bound_ports

# TODO: Why does not this test work with remote host?
def test_child_process_dies(self):
nAttempt = 0

while True:
if nAttempt == 5:
raise Exception("Max attempt number is exceed.")

nAttempt += 1

logging.info("Attempt #{0}".format(nAttempt))

# test for FileNotFound exception during child_processes() function
with subprocess.Popen(["sleep", "60"]) as process:
r = process.poll()

if r is not None:
logging.warning("process.pool() returns an unexpected result: {0}.".format(r))
continue

assert r is None
# collect list of processes currently running
children = psutil.Process(os.getpid()).children()
# kill a process, so received children dictionary becomes invalid
process.kill()
process.wait()
# try to handle children list -- missing processes will have ptype "ProcessType.Unknown"
[ProcessProxy(p) for p in children]
break

@staticmethod
def helper__get_node(name=None):
assert isinstance(__class__.sm_os_ops, OsOperations)
Expand Down