Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ endif
include packaging/Makefile.pkg
include packaging/Makefile.repo
include packaging/Makefile.test

6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
`pg_probackup` is a utility to manage backup and recovery of PostgreSQL database clusters. It is designed to perform periodic backups of the PostgreSQL instance that enable you to restore the server in case of a failure.

The utility is compatible with:
* PostgreSQL 13, 14, 15, 16, 17
* PostgreSQL 13, 14, 15, 16, 17, 18

As compared to other backup solutions, `pg_probackup` offers the following benefits that can help you implement different backup strategies and deal with large amounts of data:
* Incremental backup: page-level incremental backup allows you to save disk space, speed up backup and restore. With three different incremental modes, you can plan the backup strategy in accordance with your data flow.
Expand Down Expand Up @@ -79,7 +79,7 @@ For users of Postgres Pro products, commercial editions of pg_probackup are avai
## Building from source
### Linux

To compile `pg_probackup`, you must have a PostgreSQL installation and raw source tree. Execute this in the module's directory:
To compile `pg_probackup`, you must have a PostgreSQL installation and raw source tree. For versions under 18 execute this in the module's directory:

```shell
make USE_PGXS=1 PG_CONFIG=<path_to_pg_config> top_srcdir=<path_to_PostgreSQL_source_tree>
Expand All @@ -91,6 +91,8 @@ The alternative way, without using the PGXS infrastructure, is to place `pg_prob
cd <path_to_PostgreSQL_source_tree> && git clone https://github.com/postgrespro/pg_probackup contrib/pg_probackup && cd contrib/pg_probackup && make
```

For version 18 you have to apply PostgreSQL core patch (patches/REL_18_STABLE_pg_probackup.patch) first and recompile and reinstall PostgreSQL

### Windows

Currently pg_probackup can be build using only MSVC 2013.
Expand Down
29 changes: 29 additions & 0 deletions patches/REL_18_STABLE_pg_probackup.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
From 5809673569d3d95c7bce75ea0aa6f9723e303c7d Mon Sep 17 00:00:00 2001
From: Daria <d.lepikhova@postgrespro.ru>
Date: Mon, 17 Nov 2025 02:57:38 +0100
Subject: [PATCH] REL_18_STABLE_pg_probackup

---
src/backend/utils/hash/pg_crc.c | 2 ++
1 file changed, 2 insertions(+)

diff --git a/src/backend/utils/hash/pg_crc.c b/src/backend/utils/hash/pg_crc.c
index e67a74ef852..6a474e804b5 100644
--- a/src/backend/utils/hash/pg_crc.c
+++ b/src/backend/utils/hash/pg_crc.c
@@ -99,6 +99,7 @@ const uint32 pg_crc32_table[256] = {
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
};

+#ifndef FRONTEND
/*
* SQL-callable functions
*/
@@ -128,3 +129,4 @@ crc32c_bytea(PG_FUNCTION_ARGS)

PG_RETURN_INT64(crc);
}
+#endif
--
2.39.5 (Apple Git-154)

12 changes: 10 additions & 2 deletions src/backup.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ static bool pgpro_support(PGconn *conn);
static bool pg_is_checksum_enabled(PGconn *conn);
static bool pg_is_in_recovery(PGconn *conn);
static bool pg_is_superuser(PGconn *conn);
static void check_server_version(PGconn *conn, PGNodeInfo *nodeInfo);
static void confirm_block_size(PGconn *conn, const char *name, int blcksz);
static void rewind_and_mark_cfs_datafiles(parray *files, const char *root, char *relative, size_t i);
static bool remove_excluded_files_criterion(void *value, void *exclude_args);
Expand Down Expand Up @@ -946,7 +947,7 @@ do_backup(InstanceState *instanceState, pgSetBackupParams *set_backup_params,
/*
* Confirm that this server version is supported
*/
void
static void
check_server_version(PGconn *conn, PGNodeInfo *nodeInfo)
{
PGresult *res = NULL;
Expand Down Expand Up @@ -2517,11 +2518,16 @@ process_block_change(ForkNumber forknum, RelFileNode rnode, BlockNumber blkno)
int segno;
pgFile **file_item;
pgFile f;
#if PG_VERSION_NUM >= 180000
RelPathStr rel_path_str = relpathperm(rnode, forknum);
rel_path = rel_path_str.str;
#else
rel_path = relpathperm(rnode, forknum);
#endif

segno = blkno / RELSEG_SIZE;
blkno_inseg = blkno % RELSEG_SIZE;

rel_path = relpathperm(rnode, forknum);
if (segno > 0)
f.rel_path = psprintf("%s.%u", rel_path, segno);
else
Expand Down Expand Up @@ -2553,7 +2559,9 @@ process_block_change(ForkNumber forknum, RelFileNode rnode, BlockNumber blkno)

if (segno > 0)
pg_free(f.rel_path);
#if PG_VERSION_NUM < 180000
pg_free(rel_path);
#endif

}

Expand Down
2 changes: 1 addition & 1 deletion src/data.c
Original file line number Diff line number Diff line change
Expand Up @@ -1531,7 +1531,7 @@ validate_one_page(Page page, BlockNumber absolute_blkno,
/* Verify checksum */
page_st->checksum = pg_checksum_page(page, absolute_blkno);

if (checksum_version)
if (checksum_version && !skip_block_validation)
{
/* Checksums are enabled, so check them. */
if (page_st->checksum != ((PageHeader) page)->pd_checksum)
Expand Down
10 changes: 2 additions & 8 deletions src/pg_probackup.c
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ main(int argc, char *argv[])
if (instance_config.pgdata != NULL &&
(backup_subcmd != ARCHIVE_GET_CMD && backup_subcmd != CATCHUP_CMD) &&
!is_absolute_path(instance_config.pgdata))
elog(ERROR, "-D, --pgdata must be an absolute path");
elog(ERROR, "-D, --pgdata must be an absolute path: %s", instance_config.pgdata);

#if PG_VERSION_NUM >= 110000
/* Check xlog-seg-size option */
Expand Down Expand Up @@ -980,13 +980,7 @@ main(int argc, char *argv[])
wal_file_path, wal_file_name, batch_size, !no_validate_wal);
break;
case ADD_INSTANCE_CMD:
{
PGNodeInfo nodeInfo;
pgNodeInit(&nodeInfo);
instanceState->conn = pgut_connect(dbhost, dbport, dbname, dbuser);
check_server_version(instanceState->conn, &nodeInfo);
return do_add_instance(instanceState, &instance_config);
}
return do_add_instance(instanceState, &instance_config);
case DELETE_INSTANCE_CMD:
return do_delete_instance(instanceState);
case INIT_CMD:
Expand Down
1 change: 0 additions & 1 deletion src/pg_probackup.h
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,6 @@ extern const char *base36enc_to(long unsigned int value, char buf[ARG_SIZE_HINT
extern long unsigned int base36dec(const char *text);
extern uint32 parse_server_version(const char *server_version_str);
extern uint32 parse_program_version(const char *program_version);
void check_server_version(PGconn *conn, PGNodeInfo *nodeInfo);
extern bool parse_page(Page page, XLogRecPtr *lsn);
extern int32 do_compress(void* dst, size_t dst_size, void const* src, size_t src_size,
CompressAlg alg, int level, const char **errormsg);
Expand Down
6 changes: 5 additions & 1 deletion src/stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ CreateReplicationSlot_compat(PGconn *conn, const char *slot_name, const char *pl
bool is_temporary, bool is_physical,
bool slot_exists_ok)
{
#if PG_VERSION_NUM >= 150000
#if PG_VERSION_NUM >= 180000
return CreateReplicationSlot(conn, slot_name, plugin, is_temporary, is_physical,
/* reserve_wal = */ true, slot_exists_ok, /* two_phase = */ false, /* failover = */ false);
#elif PG_VERSION_NUM >= 150000

return CreateReplicationSlot(conn, slot_name, plugin, is_temporary, is_physical,
/* reserve_wal = */ true, slot_exists_ok, /* two_phase = */ false);
#elif PG_VERSION_NUM >= 110000
Expand Down
5 changes: 5 additions & 0 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,13 @@ digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
#endif

if (size != ControlFileSize)
{
if (size == 16384)
elog(ERROR, "Unexpected control file size %d, expected %d. Probably you trying to connect Postgres Pro using %s built with PostgreSQL. ",
(int) size, ControlFileSize, PROGRAM_NAME);
elog(ERROR, "Unexpected control file size %d, expected %d",
(int) size, ControlFileSize);
}

memcpy(ControlFile, src, sizeof(ControlFileData));

Expand Down
16 changes: 16 additions & 0 deletions src/utils/pgut.c
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,23 @@ handle_interrupt(SIGNAL_ARGS)
static void
init_cancel_handler(void)
{
#if PG_VERSION_NUM < 180000
oldhandler = pqsignal(SIGINT, handle_interrupt);
#else
{
struct sigaction act, oldact;

act.sa_handler = handle_interrupt;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;

/* Get the previous handler and set the new one */
if (sigaction(SIGINT, &act, &oldact) < 0)
elog(ERROR, "sigaction(SIGINT) failed: %m");

oldhandler = oldact.sa_handler;
}
#endif
pqsignal(SIGQUIT, handle_interrupt);
pqsignal(SIGTERM, handle_interrupt);
pqsignal(SIGPIPE, handle_interrupt);
Expand Down
6 changes: 6 additions & 0 deletions tests/archive_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,9 @@ def test_archive_get_prefetch_corruption(self):

prefetch_line = 'Prefetched WAL segment {0} is invalid, cannot use it'.format(filename)
restored_line = 'LOG: restored log file "{0}" from archive'.format(filename)

self.wait_server_wal_exists(replica.data_dir, wal_dir, filename)

tailer = tail_file(os.path.join(replica.logs_dir, 'postgresql.log'))
tailer.wait(contains=prefetch_line)
tailer.wait(contains=restored_line)
Expand Down Expand Up @@ -2483,6 +2486,9 @@ def test_archive_show_partial_files_handling(self):

self.switch_wal_segment(node)

self.wait_instance_wal_exists(backup_dir, 'node',
f"{filename}.gz")

os.rename(
os.path.join(wals_dir, filename),
os.path.join(wals_dir, '{0}.part'.format(filename)))
Expand Down
108 changes: 62 additions & 46 deletions tests/auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,51 +174,36 @@ def test_backup_via_unprivileged_user(self):
class AuthTest(unittest.TestCase):
pb = None
node = None
test_path = None

# TODO move to object scope, replace module_name
@classmethod
def setUpClass(cls):
@unittest.skipIf(skip_test, "Module pexpect isn't installed. You need to install it.")
def setUp(self):

super(AuthTest, cls).setUpClass()
super().setUp()

cls.pb = ProbackupTest()
cls.backup_dir = os.path.join(cls.pb.tmp_path, module_name, 'backup')
self.pb = ProbackupTest()
self.test_path = os.path.join(self.pb.tmp_path, module_name, self._testMethodName)
self.backup_dir = os.path.join(self.test_path, 'backup')

cls.node = cls.pb.make_simple_node(
base_dir="{}/node".format(module_name),
self.node = self.pb.make_simple_node(
base_dir=os.path.join(self.test_path, 'node'),
set_replication=True,
initdb_params=['--data-checksums', '--auth-host=md5']
)

cls.username = cls.pb.get_username()
self.modify_pg_hba(self.node, self.pb.get_username())

cls.modify_pg_hba(cls.node)

cls.pb.init_pb(cls.backup_dir)
cls.pb.add_instance(cls.backup_dir, cls.node.name, cls.node)
cls.pb.set_archiving(cls.backup_dir, cls.node.name, cls.node)
self.pb.init_pb(self.backup_dir)
self.pb.add_instance(self.backup_dir, self.node.name, self.node)
self.pb.set_archiving(self.backup_dir, self.node.name, self.node)
try:
cls.node.slow_start()
self.node.slow_start()
except StartNodeException:
raise unittest.skip("Node hasn't started")

if cls.pb.get_version(cls.node) < 100000:
cls.node.safe_psql(
"postgres",
"CREATE ROLE backup WITH LOGIN PASSWORD 'password'; "
"GRANT USAGE ON SCHEMA pg_catalog TO backup; "
"GRANT EXECUTE ON FUNCTION current_setting(text) TO backup; "
"GRANT EXECUTE ON FUNCTION pg_is_in_recovery() TO backup; "
"GRANT EXECUTE ON FUNCTION pg_start_backup(text, boolean, boolean) TO backup; "
"GRANT EXECUTE ON FUNCTION pg_stop_backup() TO backup; "
"GRANT EXECUTE ON FUNCTION pg_stop_backup(boolean) TO backup; "
"GRANT EXECUTE ON FUNCTION pg_create_restore_point(text) TO backup; "
"GRANT EXECUTE ON FUNCTION pg_switch_xlog() TO backup; "
"GRANT EXECUTE ON FUNCTION txid_current() TO backup; "
"GRANT EXECUTE ON FUNCTION txid_current_snapshot() TO backup; "
"GRANT EXECUTE ON FUNCTION txid_snapshot_xmax(txid_snapshot) TO backup;")
elif cls.pb.get_version(cls.node) < 150000:
cls.node.safe_psql(
version = self.pb.get_version(self.node)
if version < 150000:
self.node.safe_psql(
"postgres",
"CREATE ROLE backup WITH LOGIN PASSWORD 'password'; "
"GRANT USAGE ON SCHEMA pg_catalog TO backup; "
Expand All @@ -233,7 +218,7 @@ def setUpClass(cls):
"GRANT EXECUTE ON FUNCTION txid_current_snapshot() TO backup; "
"GRANT EXECUTE ON FUNCTION txid_snapshot_xmax(txid_snapshot) TO backup;")
else:
cls.node.safe_psql(
self.node.safe_psql(
"postgres",
"CREATE ROLE backup WITH LOGIN PASSWORD 'password'; "
"GRANT USAGE ON SCHEMA pg_catalog TO backup; "
Expand All @@ -247,16 +232,29 @@ def setUpClass(cls):
"GRANT EXECUTE ON FUNCTION txid_current_snapshot() TO backup; "
"GRANT EXECUTE ON FUNCTION txid_snapshot_xmax(txid_snapshot) TO backup;")

cls.pgpass_file = os.path.join(os.path.expanduser('~'), '.pgpass')

# TODO move to object scope, replace module_name
@classmethod
def tearDownClass(cls):
cls.node.cleanup()
cls.pb.del_test_dir(module_name, '')
if version >= 150000:
home_dir = os.path.join(self.test_path, "home")
os.makedirs(home_dir, exist_ok=True)
self.pb.test_env['HOME'] = home_dir
self.pgpass_file = os.path.join(home_dir, '.pgpass')
self.pgpass_file_lock = None
else:
# before PGv15 only true home dir were inspected.
# Since we can't have separate file per test, we have to serialize
# tests.
self.pgpass_file = os.path.join(os.path.expanduser('~'), '.pgpass')
self.pgpass_file_lock = self.pgpass_file + '~probackup_test_lock'
# have to lock pgpass by creating file in exclusive mode
for i in range(120):
try:
open(self.pgpass_file_lock, "x").close()
except FileExistsError:
time.sleep(1)
else:
break
else:
raise TimeoutError("can't create ~/.pgpass~probackup_test_lock for 120 seconds")

@unittest.skipIf(skip_test, "Module pexpect isn't installed. You need to install it.")
def setUp(self):
self.pb_cmd = ['backup',
'-B', self.backup_dir,
'--instance', self.node.name,
Expand All @@ -268,6 +266,19 @@ def setUp(self):
]

def tearDown(self):
if (self.pgpass_file_lock
and hasattr(self, "pgpass_line")
and os.path.exists(self.pgpass_file)):
with open(self.pgpass_file, 'r', encoding="utf-8") as fl:
lines = fl.readlines()
if self.pgpass_line in lines:
lines.remove(self.pgpass_line)
if len(lines) == 0:
os.remove(self.pgpass_file)
else:
with open(self.pgpass_file, 'w', encoding="utf-8") as fl:
fl.writelines(lines)

if "PGPASSWORD" in self.pb.test_env.keys():
del self.pb.test_env["PGPASSWORD"]

Expand All @@ -279,6 +290,10 @@ def tearDown(self):
except OSError:
pass

test_path = os.path.join(self.pb.tmp_path, module_name)
self.node.cleanup()
self.pb.del_test_dir(test_path, self._testMethodName)

def test_empty_password(self):
""" Test case: PGPB_AUTH03 - zero password length """
try:
Expand Down Expand Up @@ -313,7 +328,7 @@ def test_ctrl_c_event(self):

def test_pgpassfile_env(self):
""" Test case: PGPB_AUTH06 - set environment var PGPASSFILE """
path = os.path.join(self.pb.tmp_path, module_name, 'pgpass.conf')
path = os.path.join(self.test_path, 'pgpass.conf')
line = ":".join(['127.0.0.1', str(self.node.port), 'postgres', 'backup', 'password'])
self.create_pgpass(path, line)
self.pb.test_env["PGPASSFILE"] = path
Expand Down Expand Up @@ -367,7 +382,7 @@ def run_pb_with_auth(self, password=None, add_args = [], kill=False):


@classmethod
def modify_pg_hba(cls, node):
def modify_pg_hba(self, node, username):
"""
Description:
Add trust authentication for user postgres. Need for add new role and set grant.
Expand All @@ -378,11 +393,12 @@ def modify_pg_hba(cls, node):
with open(hba_conf, 'r+') as fio:
data = fio.read()
fio.seek(0)
fio.write('host\tall\t%s\t127.0.0.1/0\ttrust\n%s' % (cls.username, data))
fio.write('host\tall\t%s\t127.0.0.1/0\ttrust\n%s' % (username, data))


def create_pgpass(self, path, line):
self.pgpass_line = line+"\n"
with open(path, 'w') as passfile:
# host:port:db:username:password
passfile.write(line)
passfile.write(self.pgpass_line)
os.chmod(path, 0o600)
Loading
Loading