├── META.json ├── Makefile ├── README.md ├── expected └── test.out ├── pg_readonly--1.0.0.sql ├── pg_readonly.c ├── pg_readonly.conf ├── pg_readonly.control └── sql └── test.sql /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pg_readonly", 3 | "abstract": "set all cluster databases in read only", 4 | "version": "1.0.3", 5 | "maintainer": [ 6 | "Pierre Forstmann" 7 | ], 8 | "license": { 9 | "PostgreSQL": "http://www.postgresql.org/about/licence" 10 | }, 11 | "prereqs": { 12 | "runtime": { 13 | "requires": { 14 | "PostgreSQL": "9.5.21" 15 | }, 16 | "recommends": { 17 | "PostgreSQL": "9.5.21" 18 | } 19 | } 20 | }, 21 | "provides": { 22 | "pg_readonly": { 23 | "abstract": "set all cluster databases in read only", 24 | "file": "pg_readonly.c", 25 | "docfile": "README.md", 26 | "version": "1.0.3" 27 | } 28 | }, 29 | "resources": { 30 | "bugtracker": { 31 | "web": "http://github.com/pierreforstmann/pg_readonly/issues" 32 | }, 33 | "repository": { 34 | "url": "https://github.com/pierreforstmann/pg_readonly.git", 35 | "web": "https://github.com/pierreforstmann/pg_readonly", 36 | "type": "git" 37 | } 38 | }, 39 | "generated_by": "Pierre Forstmann", 40 | "meta-spec": { 41 | "version": "1.0.0", 42 | "url": "http://pgxn.org/meta/spec.txt" 43 | }, 44 | "tags": [ 45 | "read only", 46 | "cluster" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile 3 | # 4 | MODULES = pg_readonly 5 | EXTENSION = pg_readonly # the extension's name 6 | DATA = pg_readonly--1.0.0.sql # script file to install 7 | 8 | # make installcheck to run automatic test 9 | REGRESS_OPTS = --temp-instance=/tmp/5555 --port=5555 --temp-config pg_readonly.conf 10 | REGRESS = test 11 | 12 | PG_CONFIG = pg_config 13 | PGXS := $(shell $(PG_CONFIG) --pgxs) 14 | include $(PGXS) 15 | 16 | pgxn: 17 | git archive --format zip --output ../pgxn/pg_readonly/pg_readonly-1.0.3.zip master 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg_readonly 2 | pg_readonly is a PostgreSQL extension which allows to set all cluster databases read only. 3 | 4 | 5 | # Installation 6 | ## Compiling 7 | 8 | This module can be built using the standard PGXS infrastructure. For this to work, the pg_config program must be available in your $PATH: 9 | 10 | `git clone https://github.com/pierreforstmann/pg_readonly.git`
11 | `cd pg_readonly`
12 | `make`
13 | `make install`
14 | 15 | This extension has been validated with PostgreSQL 9.5, 9.6, 10, 11, 12, 13, 14, 15, 16 and 17. 16 | 17 | ## PostgreSQL setup 18 | 19 | Extension must be loaded at server level with `shared_preload_libraries` parameter: 20 |

21 | `shared_preload_libraries = 'pg_readonly'` 22 |

23 | and it must be created with following SQL statement at server level: 24 |

25 | `create extension pg_readonly;` 26 |
27 | 28 | 29 | ## Usage 30 | pg_readonly has no specific GUC.

31 | The read-only status is managed only in (shared) memory with a global flag. SQL functions are provided to set the flag, to unset the flag and to query the flag. 32 | The current version of the extension does not allow to store the read-only status in a permanent way.

33 | The flag is at cluster level: either all databases are read-only or all database are read-write (the usual setting).

34 | The read-only mode is implemented by filtering SQL statements: 35 | 40 | This means that the databases are in read-only mode at SQL level: however, the checkpointer, background writer, walwriter and the autovacuum launcher are still running; this means that the database files are not read-only and that in some cases the database may still write to disk.
41 | 42 | 43 | ## Example 44 | 45 | To query the cluster status, call the function get_cluster_readonly which returns true is the cluster is read-only and false if not:
46 | 47 | `# select get_cluster_readonly();`
48 | ` get_cluster_readonly `
49 | `----------------------`
50 | ` f`
51 | `(1 row)`
52 | 53 | To set the cluster read-only, call the function set_cluster_readonly:
54 | `# select set_cluster_readonly();`
55 | ` set_cluster_readonly `
56 | `----------------------`
57 | ` t`
58 | `(1 row)` 59 | 60 | The cluster is now read-only and only SELECT statements are allowed: 61 | 62 | `pierre=# select * from t;`
63 | ` x | y `
64 | `----+-----`
65 | ` 32 | abc`
66 | `(1 row)`
67 | 68 | `# update t set x=33 where y='abc';`
69 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
70 | `# select 1 into tmp;`
71 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
72 | `# create table tmp(c text);`
73 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
74 | 75 | To set the cluster on read-write, call the function unset_cluster_readonly: 76 | 77 | `# select unset_cluster_readonly();`
78 | ` unset_cluster_readonly `
79 | `------------------------`
80 | ` t`
81 | `(1 row)`
82 | 83 | The cluster is now read-write and any DML or DDL statement is allowed:
84 | `# update t set x=33 where y='abc';`
85 | `UPDATE 1`
86 | `# select * from t;`
87 | ` x | y `
88 | `----+-----`
89 | ` 33 | abc`
90 | `(1 row)`
91 | 92 | Note that any open transaction is cancelled by set_cluster_readonly function.
93 | The client is disconnected and gets the following message:
94 | `FATAL: terminating connection due to conflict with recovery`
95 | `DETAIL: User query might have needed to see row versions that must be removed.`
96 | `HINT: In a moment you should be able to reconnect to the database and repeat your command.`
97 | In PostgreSQL log, following messages are written:
98 | 99 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: killing all transactions ...`
100 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: PID 29569 signalled`
101 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: ... done.`
102 | `2020-04-14 16:00:14.531 CEST [29569] FATAL: terminating connection due to conflict with recovery`
103 | `2020-04-14 16:00:14.531 CEST [29569] DETAIL: User query might have needed to see row versions that must be removed.`
104 | `2020-04-14 16:00:14.531 CEST [29569] HINT: In a moment you should be able to reconnect to the database and repeat your command.`
105 | 106 | 107 | -------------------------------------------------------------------------------- /expected/test.out: -------------------------------------------------------------------------------- 1 | -- 2 | -- test.sql 3 | -- 4 | create extension pg_readonly; 5 | -- 6 | select get_cluster_readonly(); 7 | get_cluster_readonly 8 | ---------------------- 9 | f 10 | (1 row) 11 | 12 | select unset_cluster_readonly(); 13 | unset_cluster_readonly 14 | ------------------------ 15 | t 16 | (1 row) 17 | 18 | -- 19 | drop table t; 20 | ERROR: table "t" does not exist 21 | drop function f; 22 | ERROR: could not find a function named "f" 23 | -- 24 | create table t(i int); 25 | create function f(IN p int, OUT i int) returns int as $$ 26 | begin 27 | insert into t(i) values (f.p) returning t.i into f.i; 28 | end$$ 29 | language plpgsql; 30 | select set_cluster_readonly(); 31 | set_cluster_readonly 32 | ---------------------- 33 | t 34 | (1 row) 35 | 36 | select * from t; 37 | i 38 | --- 39 | (0 rows) 40 | 41 | insert into t values (1); 42 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only 43 | select f(1); 44 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only 45 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i" 46 | PL/pgSQL function f(integer) line 3 at SQL statement 47 | -- However, in this same session, if you call f() before entering in read only mode, 48 | -- it is then able to write during read only mode: 49 | select unset_cluster_readonly(); 50 | unset_cluster_readonly 51 | ------------------------ 52 | t 53 | (1 row) 54 | 55 | select f(1); 56 | f 57 | --- 58 | 1 59 | (1 row) 60 | 61 | select set_cluster_readonly(); 62 | set_cluster_readonly 63 | ---------------------- 64 | t 65 | (1 row) 66 | 67 | insert into t values (2); 68 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only 69 | select f(2); 70 | ERROR: pg_readonly: pgro_exec: invalid statement because cluster is read-only 71 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i" 72 | PL/pgSQL function f(integer) line 3 at SQL statement 73 | select get_cluster_readonly(); 74 | get_cluster_readonly 75 | ---------------------- 76 | t 77 | (1 row) 78 | 79 | select f(3); 80 | ERROR: pg_readonly: pgro_exec: invalid statement because cluster is read-only 81 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i" 82 | PL/pgSQL function f(integer) line 3 at SQL statement 83 | select * from t; 84 | i 85 | --- 86 | 1 87 | (1 row) 88 | 89 | -------------------------------------------------------------------------------- /pg_readonly--1.0.0.sql: -------------------------------------------------------------------------------- 1 | -- \echo Use "CREATE EXTENSION pg_readonly" to load this file . \quit 2 | DROP FUNCTION IF EXISTS set_cluster_readonly(); 3 | DROP FUNCTION IF EXISTS unset_cluster_readonly(); 4 | DROP FUNCTION IF EXISTS get_cluster_readonly(); 5 | -- 6 | CREATE FUNCTION set_cluster_readonly() RETURNS bool 7 | AS 'pg_readonly.so', 'pgro_set_readonly' 8 | LANGUAGE C STRICT; 9 | -- 10 | CREATE FUNCTION unset_cluster_readonly() RETURNS bool 11 | AS 'pg_readonly.so', 'pgro_unset_readonly' 12 | LANGUAGE C STRICT; 13 | -- 14 | CREATE FUNCTION get_cluster_readonly() RETURNS bool 15 | AS 'pg_readonly.so', 'pgro_get_readonly' 16 | LANGUAGE C STRICT; 17 | -- 18 | 19 | -------------------------------------------------------------------------------- /pg_readonly.c: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------- 2 | * 3 | * pg_readonly is a PostgreSQL extension which allows to set a whole 4 | * cluster read only: no INSERT,UPDATE,DELETE and no DDL can be run. 5 | * 6 | * This program is open source, licensed under the PostgreSQL license. 7 | * For license terms, see the LICENSE file. 8 | * 9 | * Copyright (c) 2020, Pierre Forstmann. 10 | * 11 | *------------------------------------------------------------------------- 12 | */ 13 | #include "postgres.h" 14 | #include "parser/analyze.h" 15 | #include "nodes/nodes.h" 16 | #include "storage/proc.h" 17 | #include "access/xact.h" 18 | 19 | #include "tcop/tcopprot.h" 20 | #include "tcop/utility.h" 21 | #include "utils/guc.h" 22 | #include "utils/snapmgr.h" 23 | #include "utils/memutils.h" 24 | #if PG_VERSION_NUM <= 90600 25 | #include "storage/lwlock.h" 26 | #endif 27 | #if PG_VERSION_NUM < 120000 28 | #include "access/transam.h" 29 | #endif 30 | 31 | #include "storage/ipc.h" 32 | #include "storage/spin.h" 33 | #include "miscadmin.h" 34 | #include "storage/procarray.h" 35 | #include "executor/executor.h" 36 | 37 | PG_MODULE_MAGIC; 38 | 39 | /* 40 | * has set_cluster_readonly() been executed 41 | * in the current backend. 42 | */ 43 | static bool read_only_flag_has_been_set = false; 44 | 45 | /* 46 | * 47 | * Global shared state 48 | * 49 | */ 50 | typedef struct pgroSharedState 51 | { 52 | LWLock *lock; /* self protection */ 53 | bool cluster_is_readonly; /* cluster read-only global flag */ 54 | } pgroSharedState; 55 | 56 | /* Saved hook values in case of unload */ 57 | #if PG_VERSION_NUM >= 150000 58 | static shmem_request_hook_type prev_shmem_request_hook = NULL; 59 | #endif 60 | static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL; 61 | static shmem_startup_hook_type prev_shmem_startup_hook = NULL; 62 | static ExecutorStart_hook_type prev_executor_start_hook = NULL; 63 | 64 | /* Links to shared memory state */ 65 | static pgroSharedState *pgro= NULL; 66 | 67 | static bool pgro_enabled = false; 68 | 69 | /*---- Function declarations ----*/ 70 | 71 | void _PG_init(void); 72 | void _PG_fini(void); 73 | 74 | static void pgro_shmem_request(void); 75 | static void pgro_shmem_startup(void); 76 | static void pgro_shmem_shutdown(int code, Datum arg); 77 | #if PG_VERSION_NUM < 140000 78 | static void pgro_main(ParseState *pstate, Query *query); 79 | #else 80 | static void pgro_main(ParseState *pstate, Query *query, JumbleState *jstate); 81 | #endif 82 | static void pgro_exec(QueryDesc *queryDesc, int eflags); 83 | 84 | static bool pgro_set_readonly_internal(); 85 | static bool pgro_unset_readonly_internal(); 86 | static bool pgro_get_readonly_internal(); 87 | 88 | PG_FUNCTION_INFO_V1(pgro_set_readonly); 89 | PG_FUNCTION_INFO_V1(pgro_unset_readonly); 90 | PG_FUNCTION_INFO_V1(pgro_get_readonly); 91 | 92 | 93 | /* 94 | * set cluster databases to read-only 95 | */ 96 | 97 | static bool pgro_set_readonly_internal() 98 | { 99 | 100 | VirtualTransactionId *tvxid; 101 | TransactionId limitXmin = InvalidTransactionId; 102 | bool excludeXmin0 = false; 103 | bool allDbs = true; 104 | int excludeVacuum = 0; 105 | int nvxids; 106 | int i; 107 | pid_t pid; 108 | 109 | elog(LOG, "pg_readonly: killing all transactions ..."); 110 | tvxid = GetCurrentVirtualXIDs( 111 | limitXmin, 112 | excludeXmin0, 113 | allDbs, 114 | excludeVacuum, 115 | &nvxids); 116 | for (i=0; i < nvxids; i++) 117 | { 118 | /* 119 | * No adequate ProcSignalReason found 120 | */ 121 | pid = CancelVirtualTransaction( 122 | tvxid[i], 123 | PROCSIG_RECOVERY_CONFLICT_SNAPSHOT); 124 | elog(LOG, "pg_readonly: PID %d signalled", pid); 125 | } 126 | elog(LOG, "pg_readonly: ... done."); 127 | 128 | 129 | LWLockAcquire(pgro->lock, LW_EXCLUSIVE); 130 | pgro->cluster_is_readonly = true; 131 | LWLockRelease(pgro->lock); 132 | return true; 133 | } 134 | 135 | 136 | /* 137 | * set cluster databases to read write 138 | */ 139 | 140 | static bool pgro_unset_readonly_internal() 141 | { 142 | LWLockAcquire(pgro->lock, LW_EXCLUSIVE); 143 | pgro->cluster_is_readonly = false; 144 | LWLockRelease(pgro->lock); 145 | return true; 146 | } 147 | 148 | 149 | /* 150 | * get cluster databases read-only or 151 | * read-write status 152 | */ 153 | 154 | static bool pgro_get_readonly_internal() 155 | { 156 | bool val; 157 | 158 | LWLockAcquire(pgro->lock, LW_SHARED); 159 | val = pgro->cluster_is_readonly; 160 | LWLockRelease(pgro->lock); 161 | return val; 162 | } 163 | 164 | /* 165 | * set cluster databases to read-only 166 | */ 167 | Datum pgro_set_readonly(PG_FUNCTION_ARGS) 168 | { 169 | if (pgro_enabled == false) 170 | { 171 | ereport(ERROR, (errmsg("pg_readonly: pgro_set_readonly: pg_readonly is not enabled"))); 172 | PG_RETURN_BOOL(false); 173 | } 174 | else 175 | { 176 | elog(DEBUG5, "pg_readonly: pgro_set_readonly: entry"); 177 | elog(DEBUG5, "pg_readonly: pgro_set_readonly: exit"); 178 | read_only_flag_has_been_set = true; 179 | PG_RETURN_BOOL(pgro_set_readonly_internal()); 180 | } 181 | } 182 | 183 | /* 184 | * set cluster databases to read-write 185 | */ 186 | Datum pgro_unset_readonly(PG_FUNCTION_ARGS) 187 | { 188 | if (pgro_enabled == false) 189 | { 190 | ereport(ERROR, (errmsg("pg_readonly: pgro_unset_readonly: pg_readonly is not enabled"))); 191 | PG_RETURN_BOOL(false); 192 | } 193 | else 194 | { 195 | elog(DEBUG5, "pg_readonly: pgro_unset_readonly: entry"); 196 | elog(DEBUG5, "pg_readonly: pgro_unset_readonly: exit"); 197 | read_only_flag_has_been_set = false; 198 | PG_RETURN_BOOL(pgro_unset_readonly_internal()); 199 | } 200 | 201 | } 202 | 203 | /* 204 | * get cluster databases status 205 | */ 206 | Datum pgro_get_readonly(PG_FUNCTION_ARGS) 207 | { 208 | if (pgro_enabled == false) 209 | { 210 | ereport(ERROR, (errmsg("pg_readonly: pgro_get_readonly: pg_readonly is not enabled"))); 211 | PG_RETURN_BOOL(false); 212 | } 213 | else 214 | { 215 | elog(DEBUG5, "pg_readonly: pgro_get_readonly: entry"); 216 | elog(DEBUG5, "pg_readonly: pgro_get_readonly: exit"); 217 | PG_RETURN_BOOL(pgro_get_readonly_internal()); 218 | } 219 | 220 | } 221 | 222 | /* 223 | ** Estimate shared memory space needed. 224 | * 225 | **/ 226 | static Size 227 | pgro_memsize(void) 228 | { 229 | Size size; 230 | 231 | size = MAXALIGN(sizeof(pgroSharedState)); 232 | 233 | return size; 234 | } 235 | 236 | /* 237 | * 238 | * shmen_request_hook 239 | */ 240 | static void 241 | pgro_shmem_request(void) 242 | { 243 | /* 244 | * Request additional shared resources. (These are no-ops if we're not in 245 | * the postmaster process.) We'll allocate or attach to the shared 246 | * resources in pgls_shmem_startup(). 247 | */ 248 | 249 | #if PG_VERSION_NUM >= 150000 250 | if (prev_shmem_request_hook) 251 | prev_shmem_request_hook(); 252 | #endif 253 | 254 | RequestAddinShmemSpace(sizeof(pgroSharedState)); 255 | #if PG_VERSION_NUM >= 90600 256 | RequestNamedLWLockTranche("pg_readonly", 1); 257 | #endif 258 | 259 | } 260 | 261 | 262 | /* 263 | * shmem_startup hook: allocate or attach to shared memory. 264 | * 265 | */ 266 | static void 267 | pgro_shmem_startup(void) 268 | { 269 | bool found; 270 | 271 | elog(DEBUG5, "pg_readonly: pgro_shmem_startup: entry"); 272 | 273 | if (prev_shmem_startup_hook) 274 | prev_shmem_startup_hook(); 275 | 276 | /* reset in case this is a restart within the postmaster */ 277 | pgro = NULL; 278 | 279 | 280 | /* 281 | ** Create or attach to the shared memory state 282 | **/ 283 | LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); 284 | 285 | pgro = ShmemInitStruct("pg_readonly", 286 | pgro_memsize(), 287 | &found); 288 | 289 | if (!found) 290 | { 291 | /* First time through ... */ 292 | #if PG_VERSION_NUM <= 90600 293 | RequestAddinLWLocks(1); 294 | pgro->lock = LWLockAssign(); 295 | #else 296 | pgro->lock = &(GetNamedLWLockTranche("pg_readonly"))->lock; 297 | #endif 298 | 299 | pgro->cluster_is_readonly = false; 300 | } 301 | 302 | LWLockRelease(AddinShmemInitLock); 303 | 304 | /* 305 | * If we're in the postmaster (or a standalone backend...), set up a shmem 306 | * exit hook (no current need ???) 307 | */ 308 | if (!IsUnderPostmaster) 309 | on_shmem_exit(pgro_shmem_shutdown, (Datum) 0); 310 | 311 | /* 312 | * Done if some other process already completed our initialization. 313 | */ 314 | if (found) 315 | return; 316 | 317 | elog(DEBUG5, "pg_readonly: pgro_shmem_startup: exit"); 318 | 319 | } 320 | 321 | /* 322 | * 323 | * shmem_shutdown hook 324 | * 325 | * Note: we don't bother with acquiring lock, because there should be no 326 | * other processes running when this is called. 327 | */ 328 | static void 329 | pgro_shmem_shutdown(int code, Datum arg) 330 | { 331 | elog(DEBUG5, "pg_readonly: pgro_shmem_shutdown: entry"); 332 | 333 | /* Don't do anything during a crash. */ 334 | if (code) 335 | return; 336 | 337 | /* Safety check ... shouldn't get here unless shmem is set up. */ 338 | if (!pgro) 339 | return; 340 | 341 | /* currently: no action */ 342 | 343 | elog(DEBUG5, "pg_readonly: pgro_shmem_shutdown: exit"); 344 | } 345 | 346 | 347 | /* 348 | * Module load callback 349 | */ 350 | void 351 | _PG_init(void) 352 | { 353 | 354 | const char *shared_preload_libraries_config; 355 | char *pg_readonly; 356 | 357 | elog(DEBUG5, "pg_readonly: _PG_init(): entry"); 358 | 359 | shared_preload_libraries_config = GetConfigOption("shared_preload_libraries", true, false); 360 | pg_readonly = strstr(shared_preload_libraries_config, "pg_readonly"); 361 | if (pg_readonly == NULL) 362 | { 363 | ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 364 | errmsg("pg_readonly: pg_readonly is not loaded"))); 365 | pgro_enabled = false; 366 | } 367 | else 368 | pgro_enabled = true; 369 | 370 | if (pgro_enabled) 371 | elog(LOG, "pg_readonly:_PG_init(): pg_readonly extension is enabled"); 372 | else ereport(LOG, (errmsg("pg_readonly:_PG_init(): pg_readonly is not enabled"))); 373 | 374 | 375 | /* 376 | ** Install hooks 377 | */ 378 | 379 | if (pgro_enabled) 380 | { 381 | #if PG_VERSION_NUM >= 150000 382 | prev_shmem_request_hook = shmem_request_hook; 383 | shmem_request_hook = pgro_shmem_request; 384 | #else 385 | pgro_shmem_request(); 386 | #endif 387 | prev_shmem_startup_hook = shmem_startup_hook; 388 | shmem_startup_hook = pgro_shmem_startup; 389 | prev_post_parse_analyze_hook = post_parse_analyze_hook; 390 | prev_executor_start_hook = ExecutorStart_hook; 391 | post_parse_analyze_hook = pgro_main; 392 | ExecutorStart_hook = pgro_exec; 393 | } 394 | 395 | elog(DEBUG5, "pg_readonly: _PG_init(): exit"); 396 | } 397 | 398 | 399 | /* 400 | * Module unload callback 401 | */ 402 | void 403 | _PG_fini(void) 404 | { 405 | elog(DEBUG5, "pg_readonly: _PG_fini(): entry"); 406 | 407 | /* Uninstall hooks. */ 408 | shmem_startup_hook = prev_shmem_startup_hook; 409 | post_parse_analyze_hook = prev_post_parse_analyze_hook; 410 | ExecutorStart_hook = prev_executor_start_hook; 411 | 412 | elog(DEBUG5, "pg_readonly: _PG_fini(): exit"); 413 | } 414 | 415 | /* 416 | * 417 | */ 418 | 419 | static void 420 | #if PG_VERSION_NUM < 140000 421 | pgro_main(ParseState *pstate, Query *query) 422 | #else 423 | pgro_main(ParseState *pstate, Query *query, JumbleState *jstate) 424 | #endif 425 | { 426 | 427 | char *sekw = "SELECT"; 428 | char *inkw = "INSERT"; 429 | char *upkw = "UPDATE"; 430 | char *dekw = "DELETE"; 431 | char *utkw = "UTILITY"; 432 | char *nokw = "NOTHING"; 433 | char *unkw = "UNKNOWN"; 434 | char *kokw = "???????"; 435 | char *kw = NULL; 436 | char *expstmt = "EXPLAIN"; 437 | char *setvstmt = "SET"; 438 | char *showvstmt = "SHOW"; 439 | char *prepstmt = "PREPARE"; 440 | char *execstmt = "EXECUTE"; 441 | char *deallocstmt = "DEALLOC"; 442 | char *otherstmt = "OTHER"; 443 | char *stmt = NULL; 444 | bool command_is_ro = false; 445 | 446 | elog(DEBUG5, "pg_readonly: pgro_main entry"); 447 | 448 | switch (query->commandType) { 449 | case CMD_UNKNOWN: 450 | kw = unkw; 451 | break; 452 | case CMD_SELECT: 453 | command_is_ro = true; 454 | kw = sekw; 455 | break; 456 | case CMD_UPDATE: 457 | kw = upkw; 458 | break; 459 | case CMD_INSERT: 460 | kw = inkw; 461 | break; 462 | case CMD_DELETE: 463 | kw = dekw; 464 | break; 465 | case CMD_UTILITY: 466 | kw = utkw; 467 | /* 468 | * allow ROLLBACK 469 | * for killed transactions 470 | */ 471 | if ( 472 | strstr((pstate->p_sourcetext), "rollback") 473 | || 474 | strstr((pstate->p_sourcetext), "ROLLBACK") 475 | ) 476 | { 477 | elog(DEBUG1, "pg_readonly: pgro_main: query->querySource=%s", 478 | pstate->p_sourcetext); 479 | command_is_ro = true; 480 | } 481 | break; 482 | case CMD_NOTHING: 483 | kw = nokw; 484 | break; 485 | default: 486 | kw = kokw; 487 | break; 488 | 489 | } 490 | elog(DEBUG1, "pg_readonly: pgro_main: query->commandType=%s", kw); 491 | elog(DEBUG1, "pg_readonly: pgro_main: command_is_ro=%d", command_is_ro); 492 | 493 | 494 | 495 | if (query->commandType == CMD_UTILITY) 496 | { 497 | switch ((nodeTag(query->utilityStmt))) 498 | { 499 | case T_ExplainStmt: 500 | stmt = expstmt; 501 | command_is_ro = true; 502 | break; 503 | case T_VariableSetStmt: 504 | stmt = setvstmt; 505 | command_is_ro = true; 506 | break; 507 | case T_VariableShowStmt: 508 | stmt = showvstmt; 509 | command_is_ro = true; 510 | break; 511 | case T_PrepareStmt: 512 | stmt = prepstmt; 513 | command_is_ro = true; 514 | break; 515 | case T_ExecuteStmt: 516 | stmt = execstmt; 517 | command_is_ro = true; 518 | break; 519 | case T_DeallocateStmt: 520 | stmt = deallocstmt; 521 | command_is_ro = true; 522 | break; 523 | default: 524 | stmt = otherstmt; 525 | break; 526 | } 527 | elog(DEBUG1, "pg_readonly: pgro_main: query->UtilityStmt=%s", stmt); 528 | elog(DEBUG1, "pg_readonly: pgro_main: command_is_ro=%d", command_is_ro); 529 | } 530 | 531 | if (pgro_get_readonly_internal() == true && command_is_ro == false) 532 | ereport(ERROR, (errmsg("pg_readonly: pgro_main: invalid statement because cluster is read-only"))); 533 | 534 | 535 | if (prev_post_parse_analyze_hook) 536 | #if PG_VERSION_NUM <= 140000 537 | (*prev_post_parse_analyze_hook)(pstate, query); 538 | #else 539 | (*prev_post_parse_analyze_hook)(pstate, query, jstate); 540 | #endif 541 | /* no "standard" call for else branch */ 542 | 543 | elog(DEBUG5, "pg_readonly: pgro_main: exit"); 544 | } 545 | 546 | static void 547 | pgro_exec(QueryDesc *queryDesc, int eflags) 548 | { 549 | char *ops="select"; 550 | char *opi="insert"; 551 | char *opu="update"; 552 | char *opd="delete"; 553 | char *opo="other"; 554 | char *op; 555 | bool command_is_ro = false; 556 | 557 | switch (queryDesc->operation) 558 | { 559 | case CMD_SELECT: 560 | op = ops; 561 | command_is_ro = true; 562 | break; 563 | case CMD_INSERT: 564 | op = opi; 565 | command_is_ro = false; 566 | break; 567 | case CMD_UPDATE: 568 | op = opu; 569 | command_is_ro = false; 570 | break; 571 | case CMD_DELETE: 572 | op = opd; 573 | command_is_ro = false; 574 | break; 575 | default: 576 | op=opo; 577 | command_is_ro = false; 578 | break; 579 | } 580 | 581 | elog(LOG, "pg_readonly: pgro_exec: qd->op %s", op); 582 | if (pgro_get_readonly_internal() == true && command_is_ro == false) 583 | ereport(ERROR, (errmsg("pg_readonly: pgro_exec: invalid statement because cluster is read-only"))); 584 | 585 | if (prev_executor_start_hook) 586 | (*prev_executor_start_hook)(queryDesc, eflags); 587 | else standard_ExecutorStart(queryDesc, eflags); 588 | 589 | } 590 | -------------------------------------------------------------------------------- /pg_readonly.conf: -------------------------------------------------------------------------------- 1 | logging_collector=on 2 | log_statement=all 3 | shared_preload_libraries='pg_readonly' 4 | -------------------------------------------------------------------------------- /pg_readonly.control: -------------------------------------------------------------------------------- 1 | # pg_readonly postgresql extension 2 | comment = 'cluster database read only' 3 | default_version = '1.0.0' 4 | module_pathname = '$libdir/pg_readonly' 5 | relocatable = false 6 | -------------------------------------------------------------------------------- /sql/test.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- test.sql 3 | -- 4 | create extension pg_readonly; 5 | -- 6 | select get_cluster_readonly(); 7 | select unset_cluster_readonly(); 8 | -- 9 | drop table t; 10 | drop function f; 11 | -- 12 | create table t(i int); 13 | 14 | create function f(IN p int, OUT i int) returns int as $$ 15 | begin 16 | insert into t(i) values (f.p) returning t.i into f.i; 17 | end$$ 18 | language plpgsql; 19 | 20 | select set_cluster_readonly(); 21 | 22 | select * from t; 23 | 24 | insert into t values (1); 25 | 26 | select f(1); 27 | -- However, in this same session, if you call f() before entering in read only mode, 28 | -- it is then able to write during read only mode: 29 | 30 | select unset_cluster_readonly(); 31 | 32 | select f(1); 33 | 34 | select set_cluster_readonly(); 35 | 36 | insert into t values (2); 37 | 38 | select f(2); 39 | 40 | select get_cluster_readonly(); 41 | 42 | select f(3); 43 | 44 | select * from t; 45 | --------------------------------------------------------------------------------