├── pg_orphaned.control ├── Makefile ├── LICENSE ├── pg_orphaned--1.0.sql ├── README.md └── pg_orphaned.c /pg_orphaned.control: -------------------------------------------------------------------------------- 1 | comment = 'deal with orphaned files' 2 | default_version = '1.0' 3 | module_pathname = '$libdir/pg_orphaned' 4 | relocatable = true 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE_big = pg_orphaned 2 | OBJS = pg_orphaned.o $(WIN32RES) 3 | 4 | EXTENSION = pg_orphaned 5 | DATA = pg_orphaned--1.0.sql 6 | PGFILEDESC = "pg_orphaned" 7 | 8 | LDFLAGS_SL += $(filter -lm, $(LIBS)) 9 | 10 | PG_CONFIG = pg_config 11 | PGXS := $(shell $(PG_CONFIG) --pgxs) 12 | include $(PGXS) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Bertrand Drouvot 2 | 3 | Permission to use, copy, modify, and distribute this software and its 4 | documentation for any purpose, without fee, and without a written agreement 5 | is hereby granted, provided that the above copyright notice and this 6 | paragraph and the following two paragraphs appear in all copies. 7 | 8 | IN NO EVENT SHALL Bertrand Drouvot BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 9 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, 10 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 11 | Bertrand Drouvot HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | 13 | Bertrand Drouvot SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED 14 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 15 | PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND Bertrand 16 | Drouvot HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 17 | ENHANCEMENTS, OR MODIFICATIONS. 18 | -------------------------------------------------------------------------------- /pg_orphaned--1.0.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION pg_list_orphaned( 2 | older_than interval default null, 3 | OUT dbname text, 4 | OUT path text, 5 | OUT name text, 6 | OUT size bigint, 7 | OUT mod_time timestamptz, 8 | OUT relfilenode bigint, 9 | OUT reloid bigint, 10 | OUT older bool) 11 | RETURNS SETOF RECORD 12 | AS 'MODULE_PATHNAME','pg_list_orphaned' 13 | LANGUAGE C VOLATILE; 14 | 15 | CREATE FUNCTION pg_list_orphaned_moved( 16 | OUT dbname text, 17 | OUT path text, 18 | OUT name text, 19 | OUT size bigint, 20 | OUT mod_time timestamptz, 21 | OUT relfilenode bigint, 22 | OUT reloid bigint) 23 | RETURNS SETOF RECORD 24 | AS 'MODULE_PATHNAME','pg_list_orphaned_moved' 25 | LANGUAGE C VOLATILE; 26 | 27 | CREATE FUNCTION pg_move_orphaned(older_than interval default null) 28 | RETURNS int 29 | LANGUAGE c 30 | AS 'MODULE_PATHNAME', 'pg_move_orphaned'; 31 | 32 | CREATE FUNCTION pg_remove_moved_orphaned() 33 | RETURNS void 34 | LANGUAGE c 35 | AS 'MODULE_PATHNAME', 'pg_remove_moved_orphaned'; 36 | 37 | CREATE FUNCTION pg_move_back_orphaned() 38 | RETURNS int 39 | LANGUAGE c 40 | AS 'MODULE_PATHNAME', 'pg_move_back_orphaned'; 41 | 42 | revoke execute on function pg_list_orphaned(older_than interval) from public; 43 | revoke execute on function pg_list_orphaned_moved() from public; 44 | revoke execute on function pg_move_orphaned(older_than interval) from public; 45 | revoke execute on function pg_remove_moved_orphaned() from public; 46 | revoke execute on function pg_move_back_orphaned() from public; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pg_orphaned 2 | =================== 3 | 4 | Features 5 | -------- 6 | 7 | Allow to manipulate orphaned files thanks to a few functions: 8 | 9 | * `pg_list_orphaned(interval)`: to list orphaned files. Orphaned files older than the interval parameter (default 1 Day) are listed with the "older" field set to true. 10 | * `pg_move_orphaned(interval)`: to move orphaned files to a "orphaned_backup" directory. Only orphaned files older than the interval parameter (default 1 Day) are moved. 11 | * `pg_list_orphaned_moved()`: to list the orphaned files that have been moved to the "orphaned_backup" directory. 12 | * `pg_move_back_orphaned()`: to move back the orphaned files from the "orphaned_backup" directory to their orginal location (if still orphaned). 13 | * `pg_remove_moved_orphaned()`: to remove the orphaned files located in the "orphaned_backup" directory. 14 | 15 | Introduction 16 | ============ 17 | 18 | If you are not aware of orphaned files, you can have a look to [this blog post first.](https://blog.dbi-services.com/can-there-be-orphaned-data-files-in-postgresql/) 19 | 20 | While you could get a list of the orphaned files with a query (as the one into the blog post mentioned above), you could get 21 | false positive due to in progress transactions (means started and not committed or rolled back yet) that are creating files (like create table, relation rewrites...). 22 | 23 | This extension is taking care of such corner cases by using a dirty snapshot while looking for the relfilnode(s) in pg_class. 24 | 25 | Installation 26 | ============ 27 | 28 | Compiling 29 | --------- 30 | 31 | The extension can be built using the standard PGXS infrastructure. For this to 32 | work, the ``pg_config`` program must be available in your $PATH. Instruction to 33 | install follows: 34 | 35 | $ git clone 36 | $ cd pg_orphaned 37 | $ make 38 | $ make install 39 | $ psql DB -c "CREATE EXTENSION pg_orphaned;" 40 | 41 | Examples 42 | ======= 43 | 44 | Example 1: 45 | ---------- 46 | 47 | ``` 48 | postgres=# create database test; 49 | CREATE DATABASE 50 | postgres=# \c test 51 | You are now connected to database "test" as user "postgres". 52 | 53 | test=# create table bdt as select * from generate_series(1,40000000); 54 | SELECT 40000000 55 | 56 | test=# select * from pg_list_orphaned() order by relfilenode; 57 | dbname | path | name | size | mod_time | relfilenode | reloid 58 | --------+------+------+------+----------+-------------+-------- 59 | (0 rows 60 | 61 | test=# begin; 62 | BEGIN 63 | test=# create table bdtorph as select * from generate_series(1,40000000); 64 | SELECT 40000000 65 | test=# create index orphidx on bdtorph(generate_series); 66 | CREATE INDEX 67 | 68 | test=# select pg_relation_filepath ('bdtorph'); 69 | pg_relation_filepath 70 | ---------------------- 71 | base/294991/294997 72 | (1 row) 73 | 74 | test=# select pg_relation_filepath ('orphidx'); 75 | pg_relation_filepath 76 | ---------------------- 77 | base/294991/295000 78 | (1 row) 79 | 80 | $ backend has been killed -9 81 | 82 | test=# select pg_relation_filepath ('orphidx'); 83 | server closed the connection unexpectedly 84 | This probably means the server terminated abnormally 85 | before or while processing the request. 86 | 87 | $ reconnect and search for orphaned files 88 | 89 | test=# select pg_relation_filepath ('orphidx'); 90 | ERROR: relation "orphidx" does not exist 91 | LINE 1: select pg_relation_filepath ('orphidx'); 92 | 93 | test=# 94 | test=# select pg_relation_filepath ('bdtorph'); 95 | ERROR: relation "bdtorph" does not exist 96 | LINE 1: select pg_relation_filepath ('bdtorph'); 97 | 98 | test=# select * from pg_list_orphaned() order by relfilenode; 99 | dbname | path | name | size | mod_time | relfilenode | reloid 100 | --------+-------------+----------+------------+------------------------+-------------+-------- 101 | test | base/294991 | 294997.1 | 376176640 | 2020-05-03 16:18:36+00 | 294997 | 0 102 | test | base/294991 | 294997 | 1073741824 | 2020-05-03 16:16:03+00 | 294997 | 0 103 | test | base/294991 | 295000 | 898490368 | 2020-05-03 16:20:16+00 | 295000 | 0 104 | (3 rows) 105 | ``` 106 | 107 | Example 2: 108 | ---------- 109 | ``` 110 | test=# CREATE TABLESPACE bdttbs location '/usr/local/pgsql12.2-orphaned/bdttbs'; 111 | CREATE TABLESPACE 112 | test=# begin; 113 | BEGIN 114 | test=# create table bdtorph tablespace bdttbs as select * from generate_series(1,40000000); 115 | SELECT 40000000 116 | 117 | test=# select pg_relation_filepath ('bdtorph'); 118 | pg_relation_filepath 119 | ------------------------------------------------ 120 | pg_tblspc/303184/PG_12_201909212/303183/303185 121 | (1 row) 122 | 123 | $ backend has been killed -9 124 | 125 | test=# select pg_relation_filepath ('bdtorph'); 126 | server closed the connection unexpectedly 127 | This probably means the server terminated abnormally 128 | before or while processing the request. 129 | 130 | $ reconnect and search for orphaned files 131 | 132 | test=# select pg_relation_filepath ('bdtorph'); 133 | ERROR: relation "bdtorph" does not exist 134 | LINE 1: select pg_relation_filepath ('bdtorph'); 135 | 136 | test=# select * from pg_list_orphaned() order by relfilenode; 137 | dbname | path | name | size | mod_time | relfilenode | reloid 138 | --------+-----------------------------------------+----------+------------+------------------------+-------------+-------- 139 | test | pg_tblspc/303184/PG_12_201909212/303183 | 303185 | 1073741824 | 2020-05-03 17:28:49+00 | 303185 | 0 140 | test | pg_tblspc/303184/PG_12_201909212/303183 | 303185.1 | 376176640 | 2020-05-03 17:30:18+00 | 303185 | 0 141 | (2 rows) 142 | ``` 143 | Example 3: 144 | ---------- 145 | ``` 146 | test=# begin; 147 | BEGIN 148 | test=# create temp table bdtorphtemp as select * from generate_series(1,40000000); 149 | SELECT 40000000 150 | 151 | test=# select pg_relation_filepath ('bdtorphtemp'); 152 | pg_relation_filepath 153 | ----------------------- 154 | base/311377/t4_311380 155 | (1 row) 156 | 157 | $ backend has been killed -9 158 | 159 | test=# select pg_relation_filepath ('bdtorphtemp'); 160 | server closed the connection unexpectedly 161 | This probably means the server terminated abnormally 162 | before or while processing the request. 163 | 164 | $ reconnect and search for orphaned files 165 | 166 | test=# select pg_relation_filepath ('bdtorphtemp'); 167 | ERROR: relation "bdtorphtemp" does not exist 168 | LINE 1: select pg_relation_filepath ('bdtorphtemp'); 169 | 170 | test=# select * from pg_list_orphaned() order by relfilenode; 171 | dbname | path | name | size | mod_time | relfilenode | reloid 172 | --------+-------------+-------------+------------+------------------------+-------------+-------- 173 | test | base/311377 | t4_311380.1 | 376176640 | 2020-05-03 17:35:03+00 | 311380 | 0 174 | test | base/311377 | t4_311380 | 1073741824 | 2020-05-03 17:34:59+00 | 311380 | 0 175 | (2 rows) 176 | ``` 177 | Example 4 (deal with in progress transaction): 178 | ---------- 179 | ``` 180 | Session 1: 181 | 182 | postgres=# begin; 183 | BEGIN 184 | postgres=*# create table bdtinpgro (a int); 185 | CREATE TABLE 186 | 187 | Session 2 would report a false orphaned file if using a query like: 188 | 189 | postgres=# select * from pg_ls_dir ( '/home/postgres/pgorph/pg_installed/data/base/13580' ) as file where file ~ '^[0-9]*$' and file::text not in (select oid::text from pg_class ); 190 | file 191 | ------- 192 | 16408 193 | (1 row) 194 | 195 | while the extension would not report this false positive: 196 | 197 | postgres=# select * from pg_list_orphaned(); 198 | dbname | path | name | size | mod_time | relfilenode | reloid 199 | --------+------+------+------+----------+-------------+-------- 200 | (0 rows) 201 | ``` 202 | Example 5 (from 10/28/2021): 203 | ---------- 204 | * pg_list_orphaned() now takes an interval as a parameter (default 1 Day). 205 | * It is used to populate the new "older" column with a boolean to indicate if the orphaned file is older than the interval. 206 | ``` 207 | postgres=# select now(); 208 | now 209 | ------------------------------- 210 | 2021-10-28 13:20:24.734192+00 211 | (1 row) 212 | 213 | postgres=# select * from pg_list_orphaned(); 214 | dbname | path | name | size | mod_time | relfilenode | reloid | older 215 | ----------+------------+-------+--------+------------------------+-------------+--------+------- 216 | postgres | base/13214 | 16391 | 106496 | 2021-10-28 13:19:56+00 | 16391 | 0 | f 217 | postgres | base/13214 | 16388 | 147456 | 2021-10-28 13:19:56+00 | 16388 | 0 | f 218 | (2 rows) 219 | 220 | postgres=# select * from pg_list_orphaned('10 seconds'); 221 | dbname | path | name | size | mod_time | relfilenode | reloid | older 222 | ----------+------------+-------+--------+------------------------+-------------+--------+------- 223 | postgres | base/13214 | 16391 | 106496 | 2021-10-28 13:19:56+00 | 16391 | 0 | t 224 | postgres | base/13214 | 16388 | 147456 | 2021-10-28 13:19:56+00 | 16388 | 0 | t 225 | (2 rows) 226 | ``` 227 | Example 6 (from 11/26/2021): 228 | ---------- 229 | Let's remove the orphaned files that are older than one minute. 230 | 231 | * list the orphaned files (older than 1 minute) 232 | ``` 233 | postgres=# select * from pg_list_orphaned('1 minute'); 234 | dbname | path | name | size | mod_time | relfilenode | reloid | older 235 | ----------+------------+----------+---------+------------------------+-------------+--------+------- 236 | postgres | base/13892 | 987654 | 8192000 | 2021-11-26 15:01:46+00 | 987654 | 0 | t 237 | postgres | base/13892 | 145676.2 | 8192000 | 2021-11-26 14:54:44+00 | 145676 | 0 | t 238 | postgres | base/13892 | 145676 | 8192000 | 2021-11-26 14:54:30+00 | 145676 | 0 | t 239 | postgres | base/13892 | 145676.1 | 8192000 | 2021-11-26 14:54:40+00 | 145676 | 0 | t 240 | (4 rows) 241 | ``` 242 | * move the orphaned files (older than one minute) to the backup directory 243 | ``` 244 | postgres=# select pg_move_orphaned('1 minute'); 245 | pg_move_orphaned 246 | ------------------ 247 | 4 248 | (1 row) 249 | ``` 250 | * list the orphaned files that are in the backup directory 251 | ``` 252 | postgres=# select * from pg_list_orphaned_moved(); 253 | dbname | path | name | size | mod_time | relfilenode | reloid 254 | ----------+----------------------------------+----------+---------+------------------------+-------------+-------- 255 | postgres | orphaned_backup/13892/base/13892 | 987654 | 8192000 | 2021-11-26 15:01:46+00 | 987654 | 0 256 | postgres | orphaned_backup/13892/base/13892 | 145676.2 | 8192000 | 2021-11-26 14:54:44+00 | 145676 | 0 257 | postgres | orphaned_backup/13892/base/13892 | 145676 | 8192000 | 2021-11-26 14:54:30+00 | 145676 | 0 258 | postgres | orphaned_backup/13892/base/13892 | 145676.1 | 8192000 | 2021-11-26 14:54:40+00 | 145676 | 0 259 | (4 rows) 260 | ``` 261 | * remove the orphaned files that have been moved to the backup directory 262 | ``` 263 | postgres=# select pg_remove_moved_orphaned(); 264 | pg_remove_moved_orphaned 265 | -------------------------- 266 | 267 | (1 row) 268 | ``` 269 | * list the orphaned files that are in the backup directory 270 | ``` 271 | postgres=# select * from pg_list_orphaned_moved(); 272 | dbname | path | name | size | mod_time | relfilenode | reloid 273 | --------+------+------+------+----------+-------------+-------- 274 | (0 rows) 275 | ``` 276 | * list the orphaned files (older than 1 minute) 277 | ``` 278 | postgres=# select * from pg_list_orphaned('1 minute'); 279 | dbname | path | name | size | mod_time | relfilenode | reloid | older 280 | --------+------+------+------+----------+-------------+--------+------- 281 | (0 rows) 282 | ``` 283 | 284 | Remarks 285 | ======= 286 | * double check `carefully` before moving or removing the files 287 | * has been tested from version 10 to 16 288 | * the functions deals with orphaned files for the database your are connected to 289 | * at the time of this writing (11/2021) there is a [commitfest entry](https://commitfest.postgresql.org/34/3228/) to avoid orphaned files 290 | 291 | License 292 | ======= 293 | 294 | pg_orphaned is free software distributed under the PostgreSQL license. 295 | 296 | Copyright (c) 2020, Bertrand Drouvot. 297 | -------------------------------------------------------------------------------- /pg_orphaned.c: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------- 2 | * 3 | * pg_orphaned.c 4 | * 5 | * This program is open source, licensed under the PostgreSQL license. 6 | * For license terms, see the LICENSE file. 7 | * 8 | *------------------------------------------------------------------------- 9 | */ 10 | 11 | #include "postgres.h" 12 | #include "miscadmin.h" 13 | #include "utils/timestamp.h" 14 | #include "funcapi.h" 15 | #include "utils/builtins.h" 16 | #include 17 | #include "commands/dbcommands.h" 18 | #include "regex/regex.h" 19 | 20 | #if PG_VERSION_NUM < 100000 21 | #include 22 | #include "storage/fd.h" 23 | #endif 24 | 25 | #if PG_VERSION_NUM < 110000 26 | #include "catalog/pg_collation.h" 27 | #include "catalog/catalog.h" 28 | #include "utils/memutils.h" 29 | #include "catalog/pg_tablespace.h" 30 | #define pg_dir_create_mode S_IRWXU 31 | #else 32 | #include "catalog/pg_collation_d.h" 33 | #include "catalog/pg_tablespace_d.h" 34 | #include "common/file_perm.h" 35 | #include "utils/rel.h" 36 | #endif 37 | 38 | #include "utils/catcache.h" 39 | #include "utils/fmgroids.h" 40 | #include "utils/relmapper.h" 41 | #include "catalog/indexing.h" 42 | #include "utils/inval.h" 43 | 44 | #if PG_VERSION_NUM >= 120000 45 | #include "access/table.h" 46 | #include "access/genam.h" 47 | #include "utils/snapmgr.h" 48 | #else 49 | #include "utils/tqual.h" 50 | #include "access/htup_details.h" 51 | #endif 52 | #define NUMBER_SUFFIXES 2 53 | #define MAX_SUFFIX_SIZE 5 54 | 55 | #include "catalog/pg_control.h" 56 | #include "common/controldata_utils.h" 57 | 58 | PG_MODULE_MAGIC; 59 | Datum pg_list_orphaned(PG_FUNCTION_ARGS); 60 | PG_FUNCTION_INFO_V1(pg_list_orphaned); 61 | 62 | Datum pg_list_orphaned_moved(PG_FUNCTION_ARGS); 63 | PG_FUNCTION_INFO_V1(pg_list_orphaned_moved); 64 | 65 | PG_FUNCTION_INFO_V1(pg_move_orphaned); 66 | Datum pg_move_orphaned(PG_FUNCTION_ARGS); 67 | 68 | PG_FUNCTION_INFO_V1(pg_remove_moved_orphaned); 69 | Datum pg_remove_moved_orphaned(PG_FUNCTION_ARGS); 70 | 71 | PG_FUNCTION_INFO_V1(pg_move_back_orphaned); 72 | Datum pg_move_back_orphaned(PG_FUNCTION_ARGS); 73 | 74 | static bool made_directory = false; 75 | static bool found_existing_directory = false; 76 | char *orphaned_backup_dir= "orphaned_backup"; 77 | static Timestamp limitts; 78 | TimestampTz last_checkpoint_time; 79 | 80 | List *list_orphaned_relations=NULL; 81 | static void pg_list_orphaned_internal(FunctionCallInfo fcinfo); 82 | static void search_orphaned(List **flist, Oid dboid, const char *dbname, const char *dir, Oid reltablespace); 83 | static void pg_build_orphaned_list(Oid dbOid, bool restore); 84 | static void verify_dir_is_empty_or_create(char *dirname, bool *created, bool *found, bool display_hint); 85 | static int pg_orphaned_mkdir_p(char *path, int omode); 86 | static int pg_orphaned_check_dir(const char *dir); 87 | static void requireSuperuser(void); 88 | static Oid RelidByRelfilenodeDirty(Oid reltablespace, Oid relfilenode); 89 | static void InitializeRelfilenodeMapDirty(void); 90 | 91 | /* Hash table for information about each relfilenode <-> oid pair */ 92 | static HTAB *RelfilenodeMapHashDirty = NULL; 93 | 94 | /* built first time through in InitializeRelfilenodeMap */ 95 | static ScanKeyData relfilenode_skey_dirty[2]; 96 | 97 | typedef struct 98 | { 99 | Oid reltablespace; 100 | Oid relfilenode; 101 | } RelfilenodeMapKeyDirty; 102 | 103 | typedef struct 104 | { 105 | RelfilenodeMapKeyDirty key; /* lookup key - must be first */ 106 | Oid relid; /* pg_class.oid */ 107 | } RelfilenodeMapEntryDirty; 108 | 109 | typedef struct OrphanedRelation { 110 | char *dbname; 111 | char *path; 112 | char *name; 113 | int size; 114 | TimestampTz mod_time; 115 | Oid relfilenode; 116 | Oid reloid; 117 | } OrphanedRelation; 118 | 119 | static void pgorph_add_suffix(List **flist, OrphanedRelation *orph); 120 | 121 | /* 122 | * function to check the status of directory 123 | * this is mainly copy/paste from existing pg_check_dir 124 | * Returns: 125 | * 0 if nonexistent 126 | * 1 if exists and empty 127 | * 2 if exists and contains _only_ dot files 128 | * 3 if exists and contains a mount point 129 | * 4 if exists and not empty 130 | * -1 if trouble accessing directory (errno reflects the error) 131 | */ 132 | int 133 | pg_orphaned_check_dir(const char *dir) 134 | { 135 | int result = 1; 136 | DIR *chkdir; 137 | struct dirent *file; 138 | bool dot_found = false; 139 | bool mount_found = false; 140 | int readdir_errno; 141 | 142 | chkdir = opendir(dir); 143 | if (chkdir == NULL) 144 | return (errno == ENOENT) ? 0 : -1; 145 | 146 | while (errno = 0, (file = readdir(chkdir)) != NULL) 147 | { 148 | if (strcmp(".", file->d_name) == 0 || 149 | strcmp("..", file->d_name) == 0) 150 | { 151 | /* skip this and parent directory */ 152 | continue; 153 | } 154 | #ifndef WIN32 155 | /* file starts with "." */ 156 | else if (file->d_name[0] == '.') 157 | { 158 | dot_found = true; 159 | } 160 | /* lost+found directory */ 161 | else if (strcmp("lost+found", file->d_name) == 0) 162 | { 163 | mount_found = true; 164 | } 165 | #endif 166 | else 167 | { 168 | /* not empty */ 169 | result = 4; 170 | break; 171 | } 172 | } 173 | 174 | /* some kind of I/O error? */ 175 | if (errno) 176 | result = -1; 177 | 178 | /* Close chkdir and avoid overwriting the readdir errno on success */ 179 | readdir_errno = errno; 180 | if (closedir(chkdir)) 181 | /* error executing closedir */ 182 | result = -1; 183 | else 184 | errno = readdir_errno; 185 | 186 | /* We report on mount point if we find a lost+found directory */ 187 | if (result == 1 && mount_found) 188 | result = 3; 189 | 190 | /* We report on dot-files if we _only_ find dot files */ 191 | if (result == 1 && dot_found) 192 | result = 2; 193 | 194 | return result; 195 | } 196 | 197 | /* 198 | * function to create directory 199 | * used to create the backup directory 200 | * mainly copy/paste from existing pg_mkdir_p 201 | * 202 | * This is equivalent to "mkdir -p" except we don't complain if the target 203 | * directory already exists. 204 | * 205 | * We assume the path is in canonical form, i.e., uses / as the separator. 206 | * 207 | * omode is the file permissions bits for the target directory. Note that any 208 | * parent directories that have to be created get permissions according to the 209 | * prevailing umask, but with u+wx forced on to ensure we can create there. 210 | * (We declare omode as int, not mode_t, to minimize dependencies for port.h.) 211 | * 212 | * Returns 0 on success, -1 (with errno set) on failure. 213 | * 214 | * Note that on failure, the path arg has been modified to show the particular 215 | * directory level we had problems with. 216 | */ 217 | int 218 | pg_orphaned_mkdir_p(char *path, int omode) 219 | { 220 | struct stat sb; 221 | mode_t numask, 222 | oumask; 223 | int last, 224 | retval; 225 | char *p; 226 | 227 | retval = 0; 228 | p = path; 229 | 230 | /* 231 | * POSIX 1003.2: For each dir operand that does not name an existing 232 | * directory, effects equivalent to those caused by the following command 233 | * shall occur: 234 | * 235 | * mkdir -p -m $(umask -S),u+wx $(dirname dir) && mkdir [-m mode] dir 236 | * 237 | * We change the user's umask and then restore it, instead of doing 238 | * chmod's. Note we assume umask() can't change errno. 239 | */ 240 | oumask = umask(0); 241 | numask = oumask & ~(S_IWUSR | S_IXUSR); 242 | (void) umask(numask); 243 | 244 | /* Skip leading '/'. */ 245 | if (p[0] == '/') 246 | ++p; 247 | for (last = 0; !last; ++p) 248 | { 249 | if (p[0] == '\0') 250 | last = 1; 251 | else if (p[0] != '/') 252 | continue; 253 | *p = '\0'; 254 | if (!last && p[1] == '\0') 255 | last = 1; 256 | 257 | if (last) 258 | (void) umask(oumask); 259 | 260 | /* check for pre-existing directory */ 261 | if (stat(path, &sb) == 0) 262 | { 263 | if (!S_ISDIR(sb.st_mode)) 264 | { 265 | if (last) 266 | errno = EEXIST; 267 | else 268 | errno = ENOTDIR; 269 | retval = -1; 270 | break; 271 | } 272 | } 273 | else if (mkdir(path, last ? omode : S_IRWXU | S_IRWXG | S_IRWXO) < 0) 274 | { 275 | retval = -1; 276 | break; 277 | } 278 | if (!last) 279 | *p = '/'; 280 | } 281 | 282 | /* ensure we restored umask */ 283 | (void) umask(oumask); 284 | 285 | return retval; 286 | } 287 | 288 | /* 289 | * function to build the list 290 | * of orphaned files 291 | * the boolean is used to search 292 | * in the standard directory or in the backup one 293 | * the logic to go through each directory/tablespace 294 | * is mainly inspired from the existing calculate_database_size() 295 | */ 296 | void 297 | pg_build_orphaned_list(Oid dbOid, bool restore) 298 | { 299 | 300 | const char *dbName = NULL; 301 | DIR *dirdesc; 302 | struct dirent *direntry; 303 | char dirpath[MAXPGPATH]; 304 | char dir[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY)]; 305 | Oid reltbsnode = InvalidOid; 306 | char *reltbsname; 307 | ControlFileData *ControlFile; 308 | bool crc_ok; 309 | time_t time_tmp; 310 | MemoryContext mctx; 311 | 312 | dbName=get_database_name(MyDatabaseId); 313 | 314 | /* get a copy of the control file */ 315 | #if PG_VERSION_NUM >= 120000 316 | ControlFile = get_controlfile(".", &crc_ok); 317 | #else 318 | ControlFile = get_controlfile(".", NULL, &crc_ok); 319 | #endif 320 | if (!crc_ok) 321 | ereport(ERROR,(errmsg("pg_control CRC value is incorrect"))); 322 | 323 | /* get last checkpoint time */ 324 | time_tmp = (time_t) ControlFile->checkPointCopy.time; 325 | last_checkpoint_time = time_t_to_timestamptz(time_tmp); 326 | 327 | mctx = MemoryContextSwitchTo(TopMemoryContext); 328 | 329 | list_free_deep(list_orphaned_relations); 330 | list_orphaned_relations=NIL; 331 | 332 | /* default tablespace */ 333 | if (!restore) 334 | snprintf(dir, sizeof(dir), "base/%u", dbOid); 335 | else 336 | snprintf(dir, sizeof(dir), "%s/%u/base/%u", orphaned_backup_dir, dbOid, dbOid); 337 | search_orphaned(&list_orphaned_relations, dbOid, dbName, dir, 0); 338 | 339 | /* Scan the non-default tablespaces */ 340 | if (!restore) 341 | snprintf(dirpath, MAXPGPATH, "pg_tblspc"); 342 | else 343 | snprintf(dirpath, MAXPGPATH, "%s/%u/pg_tblspc", orphaned_backup_dir, dbOid); 344 | 345 | /* 346 | * In case no tablespaces in the dedicated backup dir 347 | */ 348 | if (restore && pg_orphaned_check_dir(dirpath) != 4) 349 | return; 350 | 351 | dirdesc = AllocateDir(dirpath); 352 | 353 | while ((direntry = ReadDir(dirdesc, dirpath)) != NULL) 354 | { 355 | CHECK_FOR_INTERRUPTS(); 356 | 357 | if (strcmp(direntry->d_name, ".") == 0 || 358 | strcmp(direntry->d_name, "..") == 0) 359 | continue; 360 | 361 | if (!restore) 362 | snprintf(dir, sizeof(dir), "pg_tblspc/%s/%s/%u", 363 | direntry->d_name, TABLESPACE_VERSION_DIRECTORY, dbOid); 364 | else 365 | snprintf(dir, sizeof(dir), "%s/%u/pg_tblspc/%s/%s/%u", 366 | orphaned_backup_dir, dbOid, direntry->d_name, TABLESPACE_VERSION_DIRECTORY, dbOid); 367 | 368 | reltbsname = strdup(direntry->d_name); 369 | reltbsnode = (Oid) strtoul(reltbsname, &reltbsname, 10); 370 | 371 | search_orphaned(&list_orphaned_relations, dbOid, dbName, dir, reltbsnode); 372 | } 373 | FreeDir(dirdesc); 374 | MemoryContextSwitchTo(mctx); 375 | } 376 | 377 | Datum 378 | pg_list_orphaned(PG_FUNCTION_ARGS) 379 | { 380 | requireSuperuser(); 381 | 382 | if (PG_ARGISNULL(0)) 383 | limitts = GetCurrentTimestamp() - ((3600000 * 24) * (int64) 1000); // 1 Day 384 | else 385 | limitts = DatumGetTimestamp(DirectFunctionCall2(timestamp_mi_interval, TimestampGetDatum(GetCurrentTimestamp()), IntervalPGetDatum(PG_GETARG_INTERVAL_P(0)))); 386 | 387 | pg_build_orphaned_list(MyDatabaseId, false); 388 | pg_list_orphaned_internal(fcinfo); 389 | return (Datum) 0; 390 | } 391 | 392 | Datum 393 | pg_list_orphaned_moved(PG_FUNCTION_ARGS) 394 | { 395 | requireSuperuser(); 396 | 397 | pg_build_orphaned_list(MyDatabaseId, true); 398 | pg_list_orphaned_internal(fcinfo); 399 | return (Datum) 0; 400 | } 401 | 402 | void 403 | pg_list_orphaned_internal(FunctionCallInfo fcinfo) 404 | { 405 | 406 | ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; 407 | Tuplestorestate *tupstore; 408 | TupleDesc tupdesc; 409 | MemoryContext per_query_ctx; 410 | MemoryContext oldcontext; 411 | ListCell *cell; 412 | 413 | per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; 414 | oldcontext = MemoryContextSwitchTo(per_query_ctx); 415 | 416 | /* 417 | * Build a tuple descriptor for our result type 418 | */ 419 | if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) 420 | elog(ERROR, "return type must be a row type"); 421 | 422 | tupstore = tuplestore_begin_heap(true, false, work_mem); 423 | rsinfo->returnMode = SFRM_Materialize; 424 | rsinfo->setResult = tupstore; 425 | rsinfo->setDesc = tupdesc; 426 | MemoryContextSwitchTo(oldcontext); 427 | 428 | #if (PG_VERSION_NUM < 130000) 429 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(cell)) 430 | #else 431 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(list_orphaned_relations, cell)) 432 | #endif 433 | { 434 | OrphanedRelation *orph = (OrphanedRelation *)lfirst(cell); 435 | 436 | Datum values[8]; 437 | bool nulls[8]; 438 | memset(values, 0, sizeof(values)); 439 | memset(nulls, 0, sizeof(nulls)); 440 | 441 | values[0] = CStringGetTextDatum(orph->dbname); 442 | values[1] = CStringGetTextDatum(orph->path); 443 | values[2] = CStringGetTextDatum(orph->name); 444 | values[3] = Int64GetDatum(orph->size); 445 | values[4] = TimestampTzGetDatum(orph->mod_time); 446 | values[5] = Int64GetDatum(orph->relfilenode); 447 | values[6] = Int64GetDatum(orph->reloid); 448 | 449 | if (orph->mod_time <= limitts) 450 | values[7] = BoolGetDatum(true); 451 | else 452 | values[7] = BoolGetDatum(false); 453 | 454 | tuplestore_putvalues(tupstore, tupdesc, values, nulls); 455 | } 456 | } 457 | 458 | /* 459 | * function that look for orphaned files 460 | * in a given directory 461 | * the logic to go through the list of files 462 | * is mainly inspired by the existing pg_ls_dir_files() 463 | */ 464 | void 465 | search_orphaned(List **flist, Oid dboid, const char* dbname, const char* dir, Oid reltablespace) 466 | { 467 | Oid oidrel = InvalidOid; 468 | Oid relfilenode = InvalidOid; 469 | char *relfilename; 470 | DIR *dirdesc; 471 | struct dirent *de; 472 | OrphanedRelation *orph; 473 | TimestampTz segment_time; 474 | 475 | dirdesc = AllocateDir(dir); 476 | if (!dirdesc) 477 | return ; 478 | 479 | while ((de = ReadDir(dirdesc, dir)) != NULL) 480 | { 481 | char path[MAXPGPATH * 2]; 482 | struct stat attrib; 483 | 484 | /* Skip hidden files */ 485 | if (de->d_name[0] == '.') 486 | continue; 487 | 488 | /* Get the file info */ 489 | snprintf(path, sizeof(path), "%s/%s", dir, de->d_name); 490 | if (stat(path, &attrib) < 0) 491 | ereport(ERROR, 492 | (errcode_for_file_access(), 493 | errmsg("could not stat directory \"%s\": %m", dir))); 494 | 495 | /* Ignore anything but regular files */ 496 | if (!S_ISREG(attrib.st_mode)) 497 | continue; 498 | 499 | /* Ignore non digit files */ 500 | if (strstr(de->d_name, "_") == NULL && isdigit((unsigned char) *(de->d_name))) { 501 | orph = palloc(sizeof(*orph)); 502 | relfilename = strdup(de->d_name); 503 | relfilenode = (Oid) strtoul(relfilename, &relfilename, 10); 504 | /* If RelidByRelfilenodeDirty does not return a valid oid 505 | * then we consider this file as orphaned 506 | */ 507 | oidrel = RelidByRelfilenodeDirty(reltablespace, relfilenode); 508 | /* 509 | * Filter and don't report as orphaned 510 | * if first segment, size is zero and created after the last checkpoint 511 | * due to https://github.com/postgres/postgres/blob/REL_12_8/src/backend/storage/smgr/md.c#L225 512 | */ 513 | segment_time = time_t_to_timestamptz(attrib.st_mtime); 514 | if (!OidIsValid(oidrel) && !(attrib.st_size == 0 && 515 | strstr(de->d_name, ".") == NULL && segment_time > last_checkpoint_time)) 516 | { 517 | orph->dbname = strdup(dbname); 518 | orph->path = strdup(dir); 519 | orph->name = strdup(de->d_name); 520 | orph->size = (int64) attrib.st_size; 521 | orph->mod_time = segment_time; 522 | orph->relfilenode = relfilenode; 523 | orph->reloid = oidrel; 524 | *flist = lappend(*flist, orph); 525 | /* search for _init and _fsm */ 526 | if(strstr(de->d_name, ".") == NULL) 527 | pgorph_add_suffix(flist, orph); 528 | } 529 | /* 530 | * unless is this a temp table? 531 | * temp table format on disk is: t%d_%u 532 | * so that we check it starts with a t 533 | * and then check the format with a regex 534 | */ 535 | } else if (de->d_name[0] == 't') { 536 | int i; 537 | pg_wchar *wstr; 538 | int wlen; 539 | pg_wchar *regwstr; 540 | int regwlen; 541 | int r; 542 | char *regex = "^t[0-9]*_[0-9]"; 543 | int regcomp_result; 544 | char errMsg[100]; 545 | regex_t* preg = palloc(sizeof(regex_t)); 546 | char *t; 547 | char *tokptr = NULL; 548 | char *temprel; 549 | 550 | regwstr = palloc((strlen(regex) + 1) * sizeof(pg_wchar)); 551 | regwlen = pg_mb2wchar_with_len(regex, regwstr, strlen(regex)); 552 | 553 | regcomp_result = pg_regcomp(preg, 554 | regwstr, 555 | regwlen, 556 | REG_ADVANCED | REG_NOSUB, 557 | DEFAULT_COLLATION_OID); 558 | 559 | pfree(regwstr); 560 | 561 | if (regcomp_result == REG_OKAY) { 562 | wstr = palloc((strlen(de->d_name) + 1) * sizeof(pg_wchar)); 563 | wlen = pg_mb2wchar_with_len(de->d_name, wstr, strlen(de->d_name)); 564 | r = pg_regexec(preg, wstr, wlen, 0, NULL, 0, NULL, 0); 565 | if (r != REG_NOMATCH) { 566 | temprel = pstrdup(de->d_name); 567 | for (i = 0, t = strtok_r(temprel, "_", &tokptr); t != NULL; i++, t = strtok_r(NULL, "_", &tokptr)) 568 | { 569 | if (i == 1) { 570 | orph = palloc(sizeof(*orph)); 571 | relfilename = strdup(t); 572 | relfilenode = (Oid) strtoul(relfilename, &relfilename, 10); 573 | /* If RelidByRelfilenodeDirty does not return a valid oid 574 | * then we consider this file as orphaned 575 | */ 576 | oidrel = RelidByRelfilenodeDirty(reltablespace, relfilenode); 577 | if (!OidIsValid(oidrel)) { 578 | orph->dbname = strdup(dbname); 579 | orph->path = strdup(dir); 580 | orph->name = strdup(de->d_name); 581 | orph->size = (int64) attrib.st_size; 582 | orph->mod_time = time_t_to_timestamptz(attrib.st_mtime); 583 | orph->relfilenode = relfilenode; 584 | orph->reloid = oidrel; 585 | *flist = lappend(*flist, orph); 586 | /* _fsm case has already been handled for temp */ 587 | /* _init would have been too but _init on temp is not possible */ 588 | } 589 | } 590 | } 591 | } 592 | pfree(wstr); 593 | } else { 594 | /* regex didn't compile */ 595 | pg_regerror(regcomp_result, preg, errMsg, sizeof(errMsg)); 596 | ereport(ERROR, 597 | (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), 598 | errmsg("invalid regular expression: %s", errMsg))); 599 | } 600 | pg_regfree(preg); 601 | } 602 | } 603 | FreeDir(dirdesc); 604 | } 605 | 606 | /* 607 | * function to move orphaned files 608 | * to the backup directory 609 | * the exact same directory tree is kept 610 | */ 611 | Datum 612 | pg_move_orphaned(PG_FUNCTION_ARGS) 613 | { 614 | Oid dbOid; 615 | ListCell *cell; 616 | char *dir_to_create; 617 | int nb_moved; 618 | 619 | requireSuperuser(); 620 | 621 | if (PG_ARGISNULL(0)) 622 | limitts = GetCurrentTimestamp() - ((3600000 * 24) * (int64) 1000); // 1 Day 623 | else 624 | limitts = DatumGetTimestamp(DirectFunctionCall2(timestamp_mi_interval, TimestampGetDatum(GetCurrentTimestamp()), IntervalPGetDatum(PG_GETARG_INTERVAL_P(0)))); 625 | 626 | dbOid = MyDatabaseId; 627 | pg_build_orphaned_list(dbOid, false); 628 | dir_to_create = psprintf("%s/%d", orphaned_backup_dir, dbOid); 629 | 630 | verify_dir_is_empty_or_create(dir_to_create, &made_directory, &found_existing_directory, true); 631 | nb_moved = 0; 632 | 633 | /* going through the list of orphaned files */ 634 | #if (PG_VERSION_NUM < 130000) 635 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(cell)) 636 | #else 637 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(list_orphaned_relations, cell)) 638 | #endif 639 | { 640 | char orphaned_file[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY) + 10 + 6] = {0}; 641 | char orphaned_file_backup_dir[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY) + 10 + 6] = {0}; 642 | char orphaned_file_backup[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY) + 10 + 6] = {0}; 643 | 644 | OrphanedRelation *orph = (OrphanedRelation *)lfirst(cell); 645 | 646 | snprintf(orphaned_file, sizeof(orphaned_file), "%s/%s", orph->path, orph->name); 647 | snprintf(orphaned_file_backup_dir, sizeof(orphaned_file_backup_dir), "%s/%s", dir_to_create, orph->path); 648 | 649 | /* 650 | * Create the directory if it does not exist 651 | */ 652 | if (pg_orphaned_check_dir(orphaned_file_backup_dir) == 0) 653 | verify_dir_is_empty_or_create(orphaned_file_backup_dir, &made_directory, &found_existing_directory, false); 654 | 655 | snprintf(orphaned_file_backup, sizeof(orphaned_file_backup), "%s/%s", orphaned_file_backup_dir, orph->name); 656 | 657 | /* If old enough, move the orphaned file and check the success */ 658 | if (orph->mod_time <= limitts) 659 | { 660 | if (rename(orphaned_file, orphaned_file_backup) != 0) 661 | ereport(ERROR, 662 | (errcode_for_file_access(), 663 | errmsg("could not rename \"%s\" to \"%s\": %m", 664 | orphaned_file, orphaned_file_backup))); 665 | else 666 | nb_moved++; 667 | } 668 | } 669 | PG_RETURN_INT32(nb_moved); 670 | } 671 | 672 | /* 673 | * function to remove the orphaned files 674 | * we basically remove the whole backup directory 675 | * for this database 676 | */ 677 | Datum 678 | pg_remove_moved_orphaned(PG_FUNCTION_ARGS) 679 | { 680 | 681 | Oid dbOid; 682 | char *dir_to_remove; 683 | 684 | requireSuperuser(); 685 | 686 | dbOid = MyDatabaseId; 687 | 688 | dir_to_remove = psprintf("%s/%d", orphaned_backup_dir, dbOid); 689 | if (!rmtree(dir_to_remove, true)) 690 | ereport(WARNING, 691 | (errmsg("could not remove directory \"%s\"", dir_to_remove))); 692 | 693 | PG_RETURN_VOID(); 694 | } 695 | 696 | /* 697 | * function to move back the backed up 698 | * orphaned files to their original location 699 | * we ensure that the files to restore are still 700 | * orphaned ones 701 | */ 702 | Datum 703 | pg_move_back_orphaned(PG_FUNCTION_ARGS) 704 | { 705 | 706 | Oid dbOid; 707 | ListCell *cell; 708 | int nb_moved; 709 | 710 | requireSuperuser(); 711 | 712 | dbOid = MyDatabaseId; 713 | nb_moved = 0; 714 | 715 | /* 716 | * Exists but empty 717 | */ 718 | if (pg_orphaned_check_dir(orphaned_backup_dir) != 4) 719 | PG_RETURN_INT32(nb_moved); 720 | 721 | /* building the list of orphaned files 722 | * from the backup location: so the second arg is set to true 723 | */ 724 | pg_build_orphaned_list(dbOid, true); 725 | 726 | /* going through the list of orphaned files */ 727 | #if (PG_VERSION_NUM < 130000) 728 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(cell)) 729 | #else 730 | for (cell = list_head(list_orphaned_relations); cell != NULL; cell = lnext(list_orphaned_relations, cell)) 731 | #endif 732 | { 733 | char orphaned_file_backup[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY) + 10 + 6] = {0}; 734 | char *orphaned_file_restore; 735 | char *orphaned_file_restore_dup; 736 | 737 | OrphanedRelation *orph = (OrphanedRelation *)lfirst(cell); 738 | 739 | snprintf(orphaned_file_backup, sizeof(orphaned_file_backup), "%s/%s", orph->path, orph->name); 740 | 741 | /* remove first 2 directories used to locate the backup */ 742 | orphaned_file_restore_dup = strdup(orphaned_file_backup); 743 | orphaned_file_restore = strchr(orphaned_file_restore_dup, '/'); 744 | 745 | orphaned_file_restore_dup = orphaned_file_restore + 1; 746 | orphaned_file_restore = strchr(orphaned_file_restore_dup, '/'); 747 | 748 | /* move the orphaned files back to their original location */ 749 | if (rename(orphaned_file_backup, orphaned_file_restore + 1) != 0) 750 | ereport(ERROR, 751 | (errcode_for_file_access(), 752 | errmsg("could not rename \"%s\" to \"%s\": %m", 753 | orphaned_file_backup, orphaned_file_restore+1))); 754 | else 755 | nb_moved++; 756 | } 757 | PG_RETURN_INT32(nb_moved); 758 | } 759 | 760 | /* 761 | * Verify that the given directory exists and is empty. If it does not 762 | * exist, it is created. If it exists but is not empty, an error will 763 | * be given and the process ended. 764 | * mainly a copy/paste from the verify_dir_is_empty_or_create() in pg_basebackup.c 765 | */ 766 | static void 767 | verify_dir_is_empty_or_create(char *dirname, bool *created, bool *found, bool display_hint) 768 | { 769 | switch (pg_orphaned_check_dir(dirname)) 770 | { 771 | case 0: 772 | 773 | /* 774 | * Does not exist, so create 775 | */ 776 | if (pg_orphaned_mkdir_p(dirname, pg_dir_create_mode) == -1) 777 | { 778 | ereport(ERROR, 779 | (errcode_for_file_access(), 780 | errmsg("could not create directory \"%s\": %m", dirname))); 781 | exit(1); 782 | } 783 | if (created) 784 | *created = true; 785 | return; 786 | case 1: 787 | 788 | /* 789 | * Exists, empty 790 | */ 791 | if (found) 792 | *found = true; 793 | return; 794 | case 2: 795 | case 3: 796 | case 4: 797 | 798 | /* 799 | * Exists, not empty 800 | */ 801 | if (!display_hint) 802 | ereport(ERROR, 803 | (errcode_for_file_access(), 804 | errmsg("directory \"%s\" exists but is not empty", dirname))); 805 | else 806 | ereport(ERROR, 807 | (errcode_for_file_access(), 808 | errmsg("directory \"%s\" exists but is not empty", dirname), 809 | errhint(" please check no files exist with pg_list_orphaned_moved(), move them back (if any) with pg_move_back_orphaned() and then clean \"%s\" up with pg_remove_moved_orphaned()", dirname))); 810 | exit(1); 811 | case -1: 812 | 813 | /* 814 | * Access problem 815 | */ 816 | ereport(ERROR, 817 | (errcode_for_file_access(), 818 | errmsg("could not access directory \"%s\": %m", dirname))); 819 | exit(1); 820 | } 821 | } 822 | 823 | /* 824 | * We don't want non superuser to execute the functions 825 | */ 826 | static void 827 | requireSuperuser(void) 828 | { 829 | if (!superuser()) 830 | ereport(ERROR, 831 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 832 | errmsg("only superuser can execute pg_orphaned functions"))); 833 | } 834 | 835 | /* 836 | * If exists add _init, _fsm if exists 837 | * to the list of orphaned files 838 | */ 839 | void 840 | pgorph_add_suffix(List **flist, OrphanedRelation *orph) 841 | { 842 | struct stat st; 843 | char orphaned_init_fsm[MAXPGPATH + 21 + sizeof(TABLESPACE_VERSION_DIRECTORY) + 10 + 6] = {0}; 844 | char orphaned_name[10 + 6] = {0}; 845 | char add_suffix[NUMBER_SUFFIXES][MAX_SUFFIX_SIZE] = { "init", "fsm" }; 846 | int i; 847 | 848 | for (i = 0; i < NUMBER_SUFFIXES; i++) 849 | { 850 | snprintf(orphaned_init_fsm, sizeof(orphaned_init_fsm), "%s/%s_%s", orph->path, orph->name, add_suffix[i]); 851 | /* Does the corresponding file exist? */ 852 | if (lstat(orphaned_init_fsm, &st) < 0) 853 | { 854 | if (errno != ENOENT) 855 | elog(ERROR, "could not stat file \"%s\": %m", orphaned_init_fsm); 856 | } 857 | else 858 | /* file exists let's add it to the orphaned list */ 859 | { 860 | OrphanedRelation *orph_suffix; 861 | orph_suffix = palloc(sizeof(*orph_suffix)); 862 | 863 | /* most of the attributes are the same so just copy */ 864 | memcpy(orph_suffix, orph, sizeof(OrphanedRelation)); 865 | 866 | /* update specific attributes */ 867 | snprintf(orphaned_name, sizeof(orphaned_name), "%s_%s", orph_suffix->name, add_suffix[i]); 868 | orph_suffix->name = strdup(orphaned_name); 869 | orph_suffix->size = (int64) st.st_size; 870 | orph_suffix->mod_time = time_t_to_timestamptz(st.st_mtime); 871 | 872 | *flist = lappend(*flist, orph_suffix); 873 | } 874 | } 875 | } 876 | 877 | /* 878 | * Map a relation's (tablespace, filenode) to a relation's oid and cache the 879 | * result. 880 | * 881 | * This is the same as the existing RelidByRelfilenode in relfilenodemap.c but 882 | * it is done by using a DirtySnapshot as we want to see relation being created. 883 | * 884 | * Returns InvalidOid if no relation matching the criteria could be found. 885 | */ 886 | Oid 887 | RelidByRelfilenodeDirty(Oid reltablespace, Oid relfilenode) 888 | { 889 | RelfilenodeMapKeyDirty key; 890 | RelfilenodeMapEntryDirty *entry; 891 | bool found; 892 | SysScanDesc scandesc; 893 | Relation relation; 894 | HeapTuple ntp; 895 | ScanKeyData skey[2]; 896 | Oid relid; 897 | SnapshotData DirtySnapshot; 898 | 899 | InitDirtySnapshot(DirtySnapshot); 900 | if (RelfilenodeMapHashDirty == NULL) 901 | InitializeRelfilenodeMapDirty(); 902 | 903 | /* pg_class will show 0 when the value is actually MyDatabaseTableSpace */ 904 | if (reltablespace == MyDatabaseTableSpace) 905 | reltablespace = 0; 906 | 907 | MemSet(&key, 0, sizeof(key)); 908 | key.reltablespace = reltablespace; 909 | key.relfilenode = relfilenode; 910 | 911 | /* 912 | * Check cache and return entry if one is found. Even if no target 913 | * relation can be found later on we store the negative match and return a 914 | * InvalidOid from cache. That's not really necessary for performance 915 | * since querying invalid values isn't supposed to be a frequent thing, 916 | * but it's basically free. 917 | */ 918 | entry = hash_search(RelfilenodeMapHashDirty, (void *) &key, HASH_FIND, &found); 919 | 920 | if (found) 921 | return entry->relid; 922 | 923 | /* ok, no previous cache entry, do it the hard way */ 924 | 925 | /* initialize empty/negative cache entry before doing the actual lookups */ 926 | relid = InvalidOid; 927 | 928 | if (reltablespace == GLOBALTABLESPACE_OID) 929 | { 930 | /* 931 | * Ok, shared table, check relmapper. 932 | */ 933 | #if PG_VERSION_NUM >= 160000 934 | relid = RelationMapFilenumberToOid(relfilenode, true); 935 | #else 936 | relid = RelationMapFilenodeToOid(relfilenode, true); 937 | #endif 938 | } 939 | else 940 | { 941 | /* 942 | * Not a shared table, could either be a plain relation or a 943 | * non-shared, nailed one, like e.g. pg_class. 944 | */ 945 | /* check for plain relations by looking in pg_class */ 946 | #if PG_VERSION_NUM >= 120000 947 | relation = table_open(RelationRelationId, AccessShareLock); 948 | #else 949 | relation = heap_open(RelationRelationId, AccessShareLock); 950 | #endif 951 | /* copy scankey to local copy, it will be modified during the scan */ 952 | memcpy(skey, relfilenode_skey_dirty, sizeof(skey)); 953 | 954 | /* set scan arguments */ 955 | skey[0].sk_argument = ObjectIdGetDatum(reltablespace); 956 | skey[1].sk_argument = ObjectIdGetDatum(relfilenode); 957 | 958 | scandesc = systable_beginscan(relation, 959 | ClassTblspcRelfilenodeIndexId, 960 | true, 961 | &DirtySnapshot, 962 | 2, 963 | skey); 964 | 965 | found = false; 966 | 967 | while (HeapTupleIsValid(ntp = systable_getnext(scandesc))) 968 | { 969 | #if PG_VERSION_NUM >= 120000 970 | Form_pg_class classform = (Form_pg_class) GETSTRUCT(ntp); 971 | found = true; 972 | 973 | Assert(classform->reltablespace == reltablespace); 974 | Assert(classform->relfilenode == relfilenode); 975 | relid = classform->oid; 976 | #else 977 | found = true; 978 | relid = HeapTupleGetOid(ntp); 979 | #endif 980 | } 981 | 982 | systable_endscan(scandesc); 983 | #if PG_VERSION_NUM >= 120000 984 | table_close(relation, AccessShareLock); 985 | #else 986 | heap_close(relation, AccessShareLock); 987 | #endif 988 | /* check for tables that are mapped but not shared */ 989 | if (!found) 990 | #if PG_VERSION_NUM >= 160000 991 | relid = RelationMapFilenumberToOid(relfilenode, false); 992 | #else 993 | relid = RelationMapFilenodeToOid(relfilenode, false); 994 | #endif 995 | } 996 | 997 | /* 998 | * Only enter entry into cache now, our opening of pg_class could have 999 | * caused cache invalidations to be executed which would have deleted a 1000 | * new entry if we had entered it above. 1001 | */ 1002 | entry = hash_search(RelfilenodeMapHashDirty, (void *) &key, HASH_ENTER, &found); 1003 | if (found) 1004 | elog(ERROR, "corrupted hashtable"); 1005 | entry->relid = relid; 1006 | 1007 | return relid; 1008 | } 1009 | 1010 | /* 1011 | * Flush mapping entries when pg_class is updated in a relevant fashion. 1012 | * Same as RelfilenodeMapInvalidateCallback in relfilenodemap.c 1013 | */ 1014 | static void 1015 | RelfilenodeMapInvalidateCallbackDirty(Datum arg, Oid relid) 1016 | { 1017 | HASH_SEQ_STATUS status; 1018 | RelfilenodeMapEntryDirty *entry; 1019 | 1020 | /* callback only gets registered after creating the hash */ 1021 | Assert(RelfilenodeMapHashDirty != NULL); 1022 | 1023 | hash_seq_init(&status, RelfilenodeMapHashDirty); 1024 | while ((entry = (RelfilenodeMapEntryDirty *) hash_seq_search(&status)) != NULL) 1025 | { 1026 | /* 1027 | * If relid is InvalidOid, signalling a complete reset, we must remove 1028 | * all entries, otherwise just remove the specific relation's entry. 1029 | * Always remove negative cache entries. 1030 | */ 1031 | if (relid == InvalidOid || /* complete reset */ 1032 | entry->relid == InvalidOid || /* negative cache entry */ 1033 | entry->relid == relid) /* individual flushed relation */ 1034 | { 1035 | if (hash_search(RelfilenodeMapHashDirty, 1036 | (void *) &entry->key, 1037 | HASH_REMOVE, 1038 | NULL) == NULL) 1039 | elog(ERROR, "hash table corrupted"); 1040 | } 1041 | } 1042 | } 1043 | 1044 | /* 1045 | * Initialize cache, either on first use or after a reset. 1046 | * Same as InitializeRelfilenodeMap in relfilenodemap.c 1047 | */ 1048 | static void 1049 | InitializeRelfilenodeMapDirty(void) 1050 | { 1051 | HASHCTL ctl; 1052 | int i; 1053 | 1054 | /* Make sure we've initialized CacheMemoryContext. */ 1055 | if (CacheMemoryContext == NULL) 1056 | CreateCacheMemoryContext(); 1057 | 1058 | /* build skey */ 1059 | MemSet(&relfilenode_skey_dirty, 0, sizeof(relfilenode_skey_dirty)); 1060 | 1061 | for (i = 0; i < 2; i++) 1062 | { 1063 | fmgr_info_cxt(F_OIDEQ, 1064 | &relfilenode_skey_dirty[i].sk_func, 1065 | CacheMemoryContext); 1066 | relfilenode_skey_dirty[i].sk_strategy = BTEqualStrategyNumber; 1067 | relfilenode_skey_dirty[i].sk_subtype = InvalidOid; 1068 | relfilenode_skey_dirty[i].sk_collation = InvalidOid; 1069 | } 1070 | 1071 | relfilenode_skey_dirty[0].sk_attno = Anum_pg_class_reltablespace; 1072 | relfilenode_skey_dirty[1].sk_attno = Anum_pg_class_relfilenode; 1073 | 1074 | /* Initialize the hash table. */ 1075 | MemSet(&ctl, 0, sizeof(ctl)); 1076 | ctl.keysize = sizeof(RelfilenodeMapKeyDirty); 1077 | ctl.entrysize = sizeof(RelfilenodeMapEntryDirty); 1078 | ctl.hcxt = CacheMemoryContext; 1079 | 1080 | /* 1081 | * Only create the RelfilenodeMapHashDirty now, so we don't end up partially 1082 | * initialized when fmgr_info_cxt() above ERRORs out with an out of memory 1083 | * error. 1084 | * Note that the hash table is not created in shared memory but in 1085 | * private memory. 1086 | */ 1087 | RelfilenodeMapHashDirty = 1088 | hash_create("RelfilenodeMap cache", 64, &ctl, 1089 | HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); 1090 | 1091 | /* Watch for invalidation events. */ 1092 | CacheRegisterRelcacheCallback(RelfilenodeMapInvalidateCallbackDirty, 1093 | (Datum) 0); 1094 | } 1095 | --------------------------------------------------------------------------------