mirror of
https://github.com/postgres/postgres.git
synced 2025-05-05 09:19:17 +03:00
Add a macro LSN_FORMAT_ARGS for use in printf-style printing of LSNs. Convert all applicable code to use it. Reviewed-by: Ashutosh Bapat <ashutosh.bapat@enterprisedb.com> Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com> Reviewed-by: Michael Paquier <michael@paquier.xyz> Discussion: https://www.postgresql.org/message-id/flat/CAExHW5ub5NaTELZ3hJUCE6amuvqAtsSxc7O+uK7y4t9Rrk23cw@mail.gmail.com
408 lines
12 KiB
C
408 lines
12 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* backup_manifest.c
|
|
* code for generating and sending a backup manifest
|
|
*
|
|
* Portions Copyright (c) 2010-2021, PostgreSQL Global Development Group
|
|
*
|
|
* IDENTIFICATION
|
|
* src/backend/replication/backup_manifest.c
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/timeline.h"
|
|
#include "common/hex.h"
|
|
#include "libpq/libpq.h"
|
|
#include "libpq/pqformat.h"
|
|
#include "mb/pg_wchar.h"
|
|
#include "replication/backup_manifest.h"
|
|
#include "utils/json.h"
|
|
|
|
static void AppendStringToManifest(backup_manifest_info *manifest, char *s);
|
|
|
|
/*
|
|
* Does the user want a backup manifest?
|
|
*
|
|
* It's simplest to always have a manifest_info object, so that we don't need
|
|
* checks for NULL pointers in too many places. However, if the user doesn't
|
|
* want a manifest, we set manifest->buffile to NULL.
|
|
*/
|
|
static inline bool
|
|
IsManifestEnabled(backup_manifest_info *manifest)
|
|
{
|
|
return (manifest->buffile != NULL);
|
|
}
|
|
|
|
/*
|
|
* Convenience macro for appending data to the backup manifest.
|
|
*/
|
|
#define AppendToManifest(manifest, ...) \
|
|
{ \
|
|
char *_manifest_s = psprintf(__VA_ARGS__); \
|
|
AppendStringToManifest(manifest, _manifest_s); \
|
|
pfree(_manifest_s); \
|
|
}
|
|
|
|
/*
|
|
* Initialize state so that we can construct a backup manifest.
|
|
*
|
|
* NB: Although the checksum type for the data files is configurable, the
|
|
* checksum for the manifest itself always uses SHA-256. See comments in
|
|
* SendBackupManifest.
|
|
*/
|
|
void
|
|
InitializeBackupManifest(backup_manifest_info *manifest,
|
|
backup_manifest_option want_manifest,
|
|
pg_checksum_type manifest_checksum_type)
|
|
{
|
|
memset(manifest, 0, sizeof(backup_manifest_info));
|
|
manifest->checksum_type = manifest_checksum_type;
|
|
|
|
if (want_manifest == MANIFEST_OPTION_NO)
|
|
manifest->buffile = NULL;
|
|
else
|
|
{
|
|
manifest->buffile = BufFileCreateTemp(false);
|
|
manifest->manifest_ctx = pg_cryptohash_create(PG_SHA256);
|
|
if (pg_cryptohash_init(manifest->manifest_ctx) < 0)
|
|
elog(ERROR, "failed to initialize checksum of backup manifest");
|
|
}
|
|
|
|
manifest->manifest_size = UINT64CONST(0);
|
|
manifest->force_encode = (want_manifest == MANIFEST_OPTION_FORCE_ENCODE);
|
|
manifest->first_file = true;
|
|
manifest->still_checksumming = true;
|
|
|
|
if (want_manifest != MANIFEST_OPTION_NO)
|
|
AppendToManifest(manifest,
|
|
"{ \"PostgreSQL-Backup-Manifest-Version\": 1,\n"
|
|
"\"Files\": [");
|
|
}
|
|
|
|
/*
|
|
* Free resources assigned to a backup manifest constructed.
|
|
*/
|
|
void
|
|
FreeBackupManifest(backup_manifest_info *manifest)
|
|
{
|
|
pg_cryptohash_free(manifest->manifest_ctx);
|
|
manifest->manifest_ctx = NULL;
|
|
}
|
|
|
|
/*
|
|
* Add an entry to the backup manifest for a file.
|
|
*/
|
|
void
|
|
AddFileToBackupManifest(backup_manifest_info *manifest, const char *spcoid,
|
|
const char *pathname, size_t size, pg_time_t mtime,
|
|
pg_checksum_context *checksum_ctx)
|
|
{
|
|
char pathbuf[MAXPGPATH];
|
|
int pathlen;
|
|
StringInfoData buf;
|
|
|
|
if (!IsManifestEnabled(manifest))
|
|
return;
|
|
|
|
/*
|
|
* If this file is part of a tablespace, the pathname passed to this
|
|
* function will be relative to the tar file that contains it. We want the
|
|
* pathname relative to the data directory (ignoring the intermediate
|
|
* symlink traversal).
|
|
*/
|
|
if (spcoid != NULL)
|
|
{
|
|
snprintf(pathbuf, sizeof(pathbuf), "pg_tblspc/%s/%s", spcoid,
|
|
pathname);
|
|
pathname = pathbuf;
|
|
}
|
|
|
|
/*
|
|
* Each file's entry needs to be separated from any entry that follows by
|
|
* a comma, but there's no comma before the first one or after the last
|
|
* one. To make that work, adding a file to the manifest starts by
|
|
* terminating the most recently added line, with a comma if appropriate,
|
|
* but does not terminate the line inserted for this file.
|
|
*/
|
|
initStringInfo(&buf);
|
|
if (manifest->first_file)
|
|
{
|
|
appendStringInfoChar(&buf, '\n');
|
|
manifest->first_file = false;
|
|
}
|
|
else
|
|
appendStringInfoString(&buf, ",\n");
|
|
|
|
/*
|
|
* Write the relative pathname to this file out to the manifest. The
|
|
* manifest is always stored in UTF-8, so we have to encode paths that are
|
|
* not valid in that encoding.
|
|
*/
|
|
pathlen = strlen(pathname);
|
|
if (!manifest->force_encode &&
|
|
pg_verify_mbstr(PG_UTF8, pathname, pathlen, true))
|
|
{
|
|
appendStringInfoString(&buf, "{ \"Path\": ");
|
|
escape_json(&buf, pathname);
|
|
appendStringInfoString(&buf, ", ");
|
|
}
|
|
else
|
|
{
|
|
uint64 dstlen = pg_hex_enc_len(pathlen);
|
|
|
|
appendStringInfoString(&buf, "{ \"Encoded-Path\": \"");
|
|
enlargeStringInfo(&buf, dstlen);
|
|
buf.len += pg_hex_encode(pathname, pathlen,
|
|
&buf.data[buf.len], dstlen);
|
|
appendStringInfoString(&buf, "\", ");
|
|
}
|
|
|
|
appendStringInfo(&buf, "\"Size\": %zu, ", size);
|
|
|
|
/*
|
|
* Convert last modification time to a string and append it to the
|
|
* manifest. Since it's not clear what time zone to use and since time
|
|
* zone definitions can change, possibly causing confusion, use GMT
|
|
* always.
|
|
*/
|
|
appendStringInfoString(&buf, "\"Last-Modified\": \"");
|
|
enlargeStringInfo(&buf, 128);
|
|
buf.len += pg_strftime(&buf.data[buf.len], 128, "%Y-%m-%d %H:%M:%S %Z",
|
|
pg_gmtime(&mtime));
|
|
appendStringInfoChar(&buf, '"');
|
|
|
|
/* Add checksum information. */
|
|
if (checksum_ctx->type != CHECKSUM_TYPE_NONE)
|
|
{
|
|
uint8 checksumbuf[PG_CHECKSUM_MAX_LENGTH];
|
|
int checksumlen;
|
|
uint64 dstlen;
|
|
|
|
checksumlen = pg_checksum_final(checksum_ctx, checksumbuf);
|
|
if (checksumlen < 0)
|
|
elog(ERROR, "could not finalize checksum of file \"%s\"",
|
|
pathname);
|
|
|
|
appendStringInfo(&buf,
|
|
", \"Checksum-Algorithm\": \"%s\", \"Checksum\": \"",
|
|
pg_checksum_type_name(checksum_ctx->type));
|
|
dstlen = pg_hex_enc_len(checksumlen);
|
|
enlargeStringInfo(&buf, dstlen);
|
|
buf.len += pg_hex_encode((char *) checksumbuf, checksumlen,
|
|
&buf.data[buf.len], dstlen);
|
|
appendStringInfoChar(&buf, '"');
|
|
}
|
|
|
|
/* Close out the object. */
|
|
appendStringInfoString(&buf, " }");
|
|
|
|
/* OK, add it to the manifest. */
|
|
AppendStringToManifest(manifest, buf.data);
|
|
|
|
/* Avoid leaking memory. */
|
|
pfree(buf.data);
|
|
}
|
|
|
|
/*
|
|
* Add information about the WAL that will need to be replayed when restoring
|
|
* this backup to the manifest.
|
|
*/
|
|
void
|
|
AddWALInfoToBackupManifest(backup_manifest_info *manifest, XLogRecPtr startptr,
|
|
TimeLineID starttli, XLogRecPtr endptr,
|
|
TimeLineID endtli)
|
|
{
|
|
List *timelines;
|
|
ListCell *lc;
|
|
bool first_wal_range = true;
|
|
bool found_start_timeline = false;
|
|
|
|
if (!IsManifestEnabled(manifest))
|
|
return;
|
|
|
|
/* Terminate the list of files. */
|
|
AppendStringToManifest(manifest, "\n],\n");
|
|
|
|
/* Read the timeline history for the ending timeline. */
|
|
timelines = readTimeLineHistory(endtli);
|
|
|
|
/* Start a list of LSN ranges. */
|
|
AppendStringToManifest(manifest, "\"WAL-Ranges\": [\n");
|
|
|
|
foreach(lc, timelines)
|
|
{
|
|
TimeLineHistoryEntry *entry = lfirst(lc);
|
|
XLogRecPtr tl_beginptr;
|
|
|
|
/*
|
|
* We only care about timelines that were active during the backup.
|
|
* Skip any that ended before the backup started. (Note that if
|
|
* entry->end is InvalidXLogRecPtr, it means that the timeline has not
|
|
* yet ended.)
|
|
*/
|
|
if (!XLogRecPtrIsInvalid(entry->end) && entry->end < startptr)
|
|
continue;
|
|
|
|
/*
|
|
* Because the timeline history file lists newer timelines before
|
|
* older ones, the first timeline we encounter that is new enough to
|
|
* matter ought to match the ending timeline of the backup.
|
|
*/
|
|
if (first_wal_range && endtli != entry->tli)
|
|
ereport(ERROR,
|
|
errmsg("expected end timeline %u but found timeline %u",
|
|
starttli, entry->tli));
|
|
|
|
if (!XLogRecPtrIsInvalid(entry->begin))
|
|
tl_beginptr = entry->begin;
|
|
else
|
|
{
|
|
tl_beginptr = startptr;
|
|
|
|
/*
|
|
* If we reach a TLI that has no valid beginning LSN, there can't
|
|
* be any more timelines in the history after this point, so we'd
|
|
* better have arrived at the expected starting TLI. If not,
|
|
* something's gone horribly wrong.
|
|
*/
|
|
if (starttli != entry->tli)
|
|
ereport(ERROR,
|
|
errmsg("expected start timeline %u but found timeline %u",
|
|
starttli, entry->tli));
|
|
}
|
|
|
|
AppendToManifest(manifest,
|
|
"%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%X\", \"End-LSN\": \"%X/%X\" }",
|
|
first_wal_range ? "" : ",\n",
|
|
entry->tli,
|
|
LSN_FORMAT_ARGS(tl_beginptr),
|
|
LSN_FORMAT_ARGS(endptr));
|
|
|
|
if (starttli == entry->tli)
|
|
{
|
|
found_start_timeline = true;
|
|
break;
|
|
}
|
|
|
|
endptr = entry->begin;
|
|
first_wal_range = false;
|
|
}
|
|
|
|
/*
|
|
* The last entry in the timeline history for the ending timeline should
|
|
* be the ending timeline itself. Verify that this is what we observed.
|
|
*/
|
|
if (!found_start_timeline)
|
|
ereport(ERROR,
|
|
errmsg("start timeline %u not found in history of timeline %u",
|
|
starttli, endtli));
|
|
|
|
/* Terminate the list of WAL ranges. */
|
|
AppendStringToManifest(manifest, "\n],\n");
|
|
}
|
|
|
|
/*
|
|
* Finalize the backup manifest, and send it to the client.
|
|
*/
|
|
void
|
|
SendBackupManifest(backup_manifest_info *manifest)
|
|
{
|
|
StringInfoData protobuf;
|
|
uint8 checksumbuf[PG_SHA256_DIGEST_LENGTH];
|
|
char *checksumstringbuf;
|
|
size_t manifest_bytes_done = 0;
|
|
uint64 dstlen;
|
|
|
|
if (!IsManifestEnabled(manifest))
|
|
return;
|
|
|
|
/*
|
|
* Append manifest checksum, so that the problems with the manifest itself
|
|
* can be detected.
|
|
*
|
|
* We always use SHA-256 for this, regardless of what algorithm is chosen
|
|
* for checksumming the files. If we ever want to make the checksum
|
|
* algorithm used for the manifest file variable, the client will need a
|
|
* way to figure out which algorithm to use as close to the beginning of
|
|
* the manifest file as possible, to avoid having to read the whole thing
|
|
* twice.
|
|
*/
|
|
manifest->still_checksumming = false;
|
|
if (pg_cryptohash_final(manifest->manifest_ctx, checksumbuf,
|
|
sizeof(checksumbuf)) < 0)
|
|
elog(ERROR, "failed to finalize checksum of backup manifest");
|
|
AppendStringToManifest(manifest, "\"Manifest-Checksum\": \"");
|
|
dstlen = pg_hex_enc_len(sizeof(checksumbuf));
|
|
checksumstringbuf = palloc0(dstlen + 1); /* includes \0 */
|
|
pg_hex_encode((char *) checksumbuf, sizeof(checksumbuf),
|
|
checksumstringbuf, dstlen);
|
|
checksumstringbuf[dstlen] = '\0';
|
|
AppendStringToManifest(manifest, checksumstringbuf);
|
|
AppendStringToManifest(manifest, "\"}\n");
|
|
|
|
/*
|
|
* We've written all the data to the manifest file. Rewind the file so
|
|
* that we can read it all back.
|
|
*/
|
|
if (BufFileSeek(manifest->buffile, 0, 0L, SEEK_SET))
|
|
ereport(ERROR,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not rewind temporary file")));
|
|
|
|
/* Send CopyOutResponse message */
|
|
pq_beginmessage(&protobuf, 'H');
|
|
pq_sendbyte(&protobuf, 0); /* overall format */
|
|
pq_sendint16(&protobuf, 0); /* natts */
|
|
pq_endmessage(&protobuf);
|
|
|
|
/*
|
|
* Send CopyData messages.
|
|
*
|
|
* We choose to read back the data from the temporary file in chunks of
|
|
* size BLCKSZ; this isn't necessary, but buffile.c uses that as the I/O
|
|
* size, so it seems to make sense to match that value here.
|
|
*/
|
|
while (manifest_bytes_done < manifest->manifest_size)
|
|
{
|
|
char manifestbuf[BLCKSZ];
|
|
size_t bytes_to_read;
|
|
size_t rc;
|
|
|
|
bytes_to_read = Min(sizeof(manifestbuf),
|
|
manifest->manifest_size - manifest_bytes_done);
|
|
rc = BufFileRead(manifest->buffile, manifestbuf, bytes_to_read);
|
|
if (rc != bytes_to_read)
|
|
ereport(ERROR,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not read from temporary file: %m")));
|
|
pq_putmessage('d', manifestbuf, bytes_to_read);
|
|
manifest_bytes_done += bytes_to_read;
|
|
}
|
|
|
|
/* No more data, so send CopyDone message */
|
|
pq_putemptymessage('c');
|
|
|
|
/* Release resources */
|
|
BufFileClose(manifest->buffile);
|
|
}
|
|
|
|
/*
|
|
* Append a cstring to the manifest.
|
|
*/
|
|
static void
|
|
AppendStringToManifest(backup_manifest_info *manifest, char *s)
|
|
{
|
|
int len = strlen(s);
|
|
|
|
Assert(manifest != NULL);
|
|
if (manifest->still_checksumming)
|
|
{
|
|
if (pg_cryptohash_update(manifest->manifest_ctx, (uint8 *) s, len) < 0)
|
|
elog(ERROR, "failed to update checksum of backup manifest");
|
|
}
|
|
BufFileWrite(manifest->buffile, s, len);
|
|
manifest->manifest_size += len;
|
|
}
|