From 18699f684c19feab64689ebae4e1b4bd4095a69c Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 4 May 2025 15:13:42 +0300
Subject: [PATCH 1/4] [FIX] PostgresNode.__exit__ releases port after shutdown
 and cleanup

We must release a port number after the shutdown and cleanup operation not before.

It is a part of work for #249
---
 testgres/node.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 41504e8..8fdc116 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -231,8 +231,6 @@ def __enter__(self):
         return self
 
     def __exit__(self, type, value, traceback):
-        self.free_port()
-
         # NOTE: Ctrl+C does not count!
         got_exception = type is not None and type != KeyboardInterrupt
 
@@ -246,6 +244,8 @@ def __exit__(self, type, value, traceback):
         else:
             self._try_shutdown(attempts)
 
+        self.free_port()
+
     def __repr__(self):
         return "{}(name='{}', port={}, base_dir='{}')".format(
             self.__class__.__name__,

From 10ebf18f0c60b9e88db0d99e3823993045420adf Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 4 May 2025 15:23:46 +0300
Subject: [PATCH 2/4] PostgresNode::_release_resource is added

This private method releases all the allocated node resources (port and so on).

PostgresNode::__exit__ calls this new method instead free_port.
---
 testgres/node.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/testgres/node.py b/testgres/node.py
index 8fdc116..65611b3 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -244,7 +244,7 @@ def __exit__(self, type, value, traceback):
         else:
             self._try_shutdown(attempts)
 
-        self.free_port()
+        self._release_resources()
 
     def __repr__(self):
         return "{}(name='{}', port={}, base_dir='{}')".format(
@@ -663,6 +663,9 @@ def _try_shutdown(self, max_attempts, with_force=False):
             ps_output,
             ps_command)
 
+    def _release_resources(self):
+        self.free_port()
+
     @staticmethod
     def _throw_bugcheck__unexpected_result_of_ps(result, cmd):
         assert type(result) == str  # noqa: E721

From 102289bc7292d8bca6ded4fe430acce4fdd2cfca Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 4 May 2025 15:26:43 +0300
Subject: [PATCH 3/4] The new argument 'release_resources' of
 PostgresNode::cleanup method is added

When this argument is True, cleanup calls _release_resources method.

Default value is False.
---
 testgres/node.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/testgres/node.py b/testgres/node.py
index 65611b3..defc0b4 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -1343,7 +1343,7 @@ def free_port(self):
             self._port = None
             self._port_manager.release_port(port)
 
-    def cleanup(self, max_attempts=3, full=False):
+    def cleanup(self, max_attempts=3, full=False, release_resources=False):
         """
         Stop node if needed and remove its data/logs directory.
         NOTE: take a look at TestgresConfig.node_cleanup_full.
@@ -1366,6 +1366,9 @@ def cleanup(self, max_attempts=3, full=False):
 
         self.os_ops.rmdirs(rm_dir, ignore_errors=False)
 
+        if release_resources:
+            self._release_resources()
+
         return self
 
     @method_decorator(positional_args_hack(['dbname', 'query']))

From 02f7f00d380c7455d42ae17480880afdf2be733f Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 4 May 2025 15:33:27 +0300
Subject: [PATCH 4/4] [#249] NodeBackup::spawn_replica does not release a
 reserved port number during failure

NodeBackup::spawn_replica uses an explict "rollback" code to destroy a newly allocated node to release a reserved port number.
---
 testgres/backup.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/testgres/backup.py b/testgres/backup.py
index 388697b..857c46d 100644
--- a/testgres/backup.py
+++ b/testgres/backup.py
@@ -184,14 +184,19 @@ def spawn_replica(self, name=None, destroy=True, slot=None):
         """
 
         # Build a new PostgresNode
-        with clean_on_error(self.spawn_primary(name=name,
-                                               destroy=destroy)) as node:
+        node = self.spawn_primary(name=name, destroy=destroy)
+        assert node is not None
 
+        try:
             # Assign it a master and a recovery file (private magic)
             node._assign_master(self.original_node)
             node._create_recovery_conf(username=self.username, slot=slot)
+        except:  # noqa: E722
+            # TODO: Pass 'final=True' ?
+            node.cleanup(release_resources=True)
+            raise
 
-            return node
+        return node
 
     def cleanup(self):
         """