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
76 changes: 55 additions & 21 deletions docs/managing/read_only.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,82 @@
# Using Spock in Read-Only Mode

Spock supports operating a cluster in read-only mode. Read-only status is managed using a GUC (Grand Unified Configuration) parameter named `spock.readonly`. This parameter can be set to enable or disable the read-only mode. Read-only mode restricts non-superusers to read-only operations, while superusers can still perform both read and write operations regardless of the setting.
Spock supports operating a node in read-only mode. Read-only status is managed using a GUC parameter named `spock.readonly`, which can be set to one of three values:

The flag is at cluster level: either all databases are read-only or all databases
are read-write (the usual setting).
| Value | Description |
|---------|-------------|
| `off` | No restrictions. All users may write. This is the default. |
| `local` | Non-superuser local sessions are read-only. Replicated writes from apply workers are still permitted, so inbound replication continues normally. Superusers may still perform write operations. (The legacy alias `user` is accepted for backward compatibility.) |
| `all` | Non-superuser local sessions and apply workers are blocked from writing. Superusers may still perform write operations. Use this mode when you need to stop both local application writes and inbound replication. |

Read-only mode is implemented by filtering SQL statements:
The setting is at cluster level: either all databases are read-only or all
databases are read-write (the usual setting).

- `SELECT` statements are allowed if they don't call functions that write.
- DML (`INSERT`, `UPDATE`, `DELETE`) and DDL statements including `TRUNCATE` are forbidden entirely.
- DCL statements `GRANT` and `REVOKE` are also forbidden.
Read-only mode is enforced by setting PostgreSQL's `transaction_read_only` flag for affected sessions. This means that any statement that would modify data — including DML (`INSERT`, `UPDATE`, `DELETE`), DDL, `TRUNCATE`, and DCL (`GRANT`, `REVOKE`) — will be rejected by PostgreSQL's standard read-only transaction checks.

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.
The databases are in read-only mode at the 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.

## Setting Read-Only Mode

You can control read-only mode with the Spock parameter `spock.readonly`; only a superuser can modify this setting. When the cluster is set to read-only mode, non-superusers will be restricted to read-only operations, while superusers will still be able to perform read and write operations regardless of the setting.

This value can be changed using the `ALTER SYSTEM` command.
Only a superuser can modify the `spock.readonly` parameter. The value can be changed using the `ALTER SYSTEM` command:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The session-local example needs one caveat.

Only superusers can change spock.readonly, and the same page later says superusers are exempt from enforcement. As written, Lines 39-42 read like they will make the current session read-only, when that only becomes meaningful after switching to a non-superuser role.

Suggested clarification
-To set the mode for the current session only:
+To change the setting in the current backend only:
 
 ```sql
 SET spock.readonly TO local;
 ```
+
+Because superusers are exempt from `spock.readonly`, this is mainly useful
+after the session switches to a non-superuser role with `SET ROLE`.

Also applies to: 39-42, 53-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/managing/read_only.md` at line 23, Clarify that changing spock.readonly
(via ALTER SYSTEM or the session-local SET spock.readonly TO local example) can
only be done by a superuser and that the session-local setting is effectively
enforced only after the session switches to a non-superuser role (e.g., after a
SET ROLE). Update the sentence around the SET spock.readonly TO local example
and the repeated passages at lines referenced (the session-local example and the
later mentions) to add a brief caveat: superusers are exempt from
spock.readonly, so the local setting is mainly useful once the session changes
to a non-superuser role.


```sql
ALTER SYSTEM SET spock.readonly = 'on';
-- Block non-superuser local writes; inbound replication continues
ALTER SYSTEM SET spock.readonly = 'local';
SELECT pg_reload_conf();

-- Block all non-superuser writes including replication
ALTER SYSTEM SET spock.readonly = 'all';
SELECT pg_reload_conf();

-- Restore normal operation
ALTER SYSTEM SET spock.readonly = 'off';
SELECT pg_reload_conf();
```

To set the cluster to read-only mode for a session, use the `SET` command. Here are the steps:
To set the mode for the current session only:

```sql
SET spock.readonly TO on;
SET spock.readonly TO local;
```

To query the current status of the cluster, you can use the following SQL command:
To query the current status:

```sql
SHOW spock.readonly;
```

This command will return on if the cluster is in read-only mode and off if it is not.
## Superuser writes and outbound replication

In both `local` and `all` modes, superusers are exempt from the read-only
restriction and may perform write operations. **The readonly setting has no
effect on the walsender (outbound replication).** Any writes made by a
superuser are captured in WAL and will be replicated outbound to other nodes
in the cluster, regardless of the readonly mode.

To perform repair operations that should **not** replicate to other nodes, use
[`spock.repair_mode()`](../spock_functions/index.md) to suppress outbound
replication of DML/DDL statements:

```sql
BEGIN;
SELECT spock.repair_mode(true);

-- Perform repair DML/DDL here...

SELECT spock.repair_mode(false);
COMMIT;
```

## Behavior of `all` mode

In `all` mode, apply workers detect the setting and stop consuming inbound WAL.
When the mode is switched back to `off` or `local`, replication resumes from
where it left off — no data is lost.

Notes:
- Only superusers can set and unset the `spock.readonly` parameter.
- When the cluster is in read-only mode, only non-superusers are restricted to read-only operations. Superusers can continue to perform both read and write operations.
- By using a GUC parameter, you can easily manage the cluster's read-only status through standard PostgreSQL configuration mechanisms.
- By using a GUC parameter, you can easily manage the node's read-only status through standard PostgreSQL configuration mechanisms.

15 changes: 14 additions & 1 deletion include/spock_readonly.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,23 @@
#include "spock_relcache.h"


/*
* SpockReadonlyMode --- controls write restrictions on the node.
*
* READONLY_OFF No restrictions; all users may write.
* READONLY_LOCAL Non-superuser local sessions are read-only; replicated
* writes from apply workers are still permitted.
* (GUC values "local" and the legacy alias "user".)
* READONLY_ALL The node is fully read-only: both local sessions and
* apply workers are blocked from writing.
Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

READONLY_ALL comment doesn't match the implementation.

src/spock_readonly.c only forces XactReadOnly for !superuser(), so local superuser sessions are still exempt here. Tightening the comment avoids contradicting both the code and the docs.

Suggested wording
- * READONLY_ALL		The node is fully read-only: both local sessions and
- *					apply workers are blocked from writing.
+ * READONLY_ALL		Non-superuser local sessions and apply workers are
+ *					blocked from writing. Superuser local sessions are
+ *					still exempt.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/spock_readonly.h` around lines 18 - 19, The READONLY_ALL comment in
include/spock_readonly.h incorrectly states the node is "fully read-only" while
the implementation in src/spock_readonly.c only forces XactReadOnly for
non-superusers (i.e. superuser() sessions remain exempt); update the README_ALL
description to reflect that it blocks local sessions and apply workers for
non-superusers (or otherwise note the superuser exemption) so the header matches
the behavior of XactReadOnly, superuser(), and the docs.

*
* The values are ordered so that (spock_readonly >= READONLY_LOCAL) is a
* convenient test for "any read-only mode is active".
*/
typedef enum SpockReadonlyMode
{
READONLY_OFF,
READONLY_USER,
READONLY_LOCAL,
READONLY_ALL
} SpockReadonlyMode;

Expand Down
3 changes: 2 additions & 1 deletion src/spock.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ static const struct config_enum_entry exception_logging_options[] = {

static const struct config_enum_entry readonly_options[] = {
{"off", READONLY_OFF, false},
{"user", READONLY_USER, false},
{"local", READONLY_LOCAL, false},
{"user", READONLY_LOCAL, true}, /* backward-compatible alias */
{"all", READONLY_ALL, false},
{NULL, 0, false}
};
Expand Down
51 changes: 42 additions & 9 deletions src/spock_apply.c
Original file line number Diff line number Diff line change
Expand Up @@ -2434,7 +2434,7 @@ replication_handler(StringInfo s)
char action = pq_getmsgbyte(s);

if (spock_readonly == READONLY_ALL)
elog(ERROR, "SPOCK %s: cluster is in read-only mode, not performing replication",
elog(FATAL, "SPOCK %s: cluster is in read-only mode, not performing replication",
MySubscription->name);

memset(&errcallback_arg, 0, sizeof(struct ActionErrCallbackArg));
Expand Down Expand Up @@ -2891,9 +2891,50 @@ apply_work(PGconn *streamConn)
StringInfo msg;
int c;

CHECK_FOR_INTERRUPTS();

if (got_SIGTERM)
break;

if (ConfigReloadPending)
{
ConfigReloadPending = false;
ProcessConfigFile(PGC_SIGHUP);
}

/*
* Do not apply new transactions if cluster is switched to
* the readonly mode.
*/
if (spock_readonly == READONLY_ALL)
{
/*
* Send feedback to keep walsender alive - we may avoid it
* with introduction of TCP keepalive approach.
*/
maybe_send_feedback(applyconn, last_received,
&last_receive_timestamp);

/*
* In case of an exception we can't break out of the loop
* because exception processing code may also modify the
* database. Wait briefly and continue to the next iteration.
*/
if (xact_had_exception)
{
rc = WaitLatch(&MyProc->procLatch, WL_LATCH_SET |
WL_TIMEOUT | WL_POSTMASTER_DEATH, 1000L);

ResetLatch(&MyProc->procLatch);

if (rc & WL_POSTMASTER_DEATH)
proc_exit(1);

continue;
}
break;
}

if (apply_replay_next == NULL)
{
char *buf;
Expand Down Expand Up @@ -2949,12 +2990,6 @@ apply_work(PGconn *streamConn)
queue_append = false;
}

if (ConfigReloadPending)
{
ConfigReloadPending = false;
ProcessConfigFile(PGC_SIGHUP);
}

/* Handle the message received or replayed */
msg = &entry->copydata;
msg->cursor = 0;
Expand Down Expand Up @@ -3072,8 +3107,6 @@ apply_work(PGconn *streamConn)

/* We must not have fallen out of MessageContext by accident */
Assert(CurrentMemoryContext == MessageContext);

CHECK_FOR_INTERRUPTS();
}

if (xact_had_exception)
Expand Down
95 changes: 44 additions & 51 deletions src/spock_readonly.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
* spock_readonly.c
* Spock readonly related functions
*
* Spock readonly functions allow setting the entire cluster to read-only mode,
* preventing INSERT, UPDATE, DELETE, and DDL operations. This file is part of
* pgEdge, Inc. open source project, licensed under the PostgreSQL license.
* Spock readonly functions allow setting the database to read-only mode,
* preventing INSERT, UPDATE, DELETE, and DDL operations.
* In the 'ALL' mode it prevents the database from being modified by spock
* apply workers as well.
* This code employs the transaction_read_only GUC to disable attempts
* to execute DML or DDL commands.
*
* This file is part of pgEdge, Inc. open source project, licensed under
* the PostgreSQL license. For license terms, see the LICENSE file.
Expand Down Expand Up @@ -62,63 +65,53 @@ spockro_terminate_active_transactions(PG_FUNCTION_ARGS)
void
spock_ropost_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
{
bool command_is_ro = false;
/*
* If spock.readonly is set, enforce Postgres core restriction for the
* following query. We actively employ the fact that the core uses the
* XactReadOnly value directly, not through the GetConfigOption function.
* Also, we use this fact here to identify if XactReadOnly has been changed
* by Spock or by external tools.
*/
if (spock_readonly >= READONLY_LOCAL && !superuser())
XactReadOnly = true;
else if (XactReadOnly)
{
const char *value =
GetConfigOption("transaction_read_only", false, false);

switch (query->commandType)
if (strcmp(value, "off") == 0)
/* Spock imposed read-only. Restore the original state. */
XactReadOnly = false;
}
else
{
case CMD_SELECT:
command_is_ro = true;
break;
case CMD_UTILITY:
switch (nodeTag(query->utilityStmt))
{
case T_AlterSystemStmt:
case T_DeallocateStmt:
case T_ExecuteStmt:
case T_ExplainStmt:
case T_PrepareStmt:
case T_TransactionStmt:
case T_VariableSetStmt:
case T_VariableShowStmt:
command_is_ro = true;
break;
case T_CopyStmt:
/*
* COPY TO (stmt->is_from=false) is a read operation, allow it.
* COPY FROM (stmt->is_from=true) is a write operation, block it.
*/
command_is_ro = !((CopyStmt *) query->utilityStmt)->is_from;
break;
default:
command_is_ro = false;
break;
}
break;
default:
command_is_ro = false;
break;
/* XactReadOnly is already false, nothing to restore. */
}
if (spock_readonly >= READONLY_USER && !command_is_ro)
ereport(ERROR, (errmsg("spock: invalid statement for a read-only cluster")));
}

void
spock_roExecutorStart(QueryDesc *queryDesc, int eflags)
{
bool command_is_ro = false;
/*
* Let's do the same job as at parse analysis hook.
*
* In some cases parse analysis and planning may be skipped on repeated
* execution (remember SPI plan for example). So, additional control makes
* sense here.
*/
if (spock_readonly >= READONLY_LOCAL && !superuser())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When spock.readonly = 'all', a superuser can still execute DDL/DML, Which shouldn't be the case right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the refactoring precisely clarifies the case: LOCAL means only backends are forbidden to write; ALL means LR processes too. Superusers can write at any time they want. Disallowing that is too dangerous: in case of a bug, the only way to change mode might be SIGKILL. So, this refactoring attempts to make this feature safer.

XactReadOnly = true;
else if (XactReadOnly)
{
const char *value =
GetConfigOption("transaction_read_only", false, false);

switch (queryDesc->operation)
if (strcmp(value, "off") == 0)
/* Spock imposed read-only. Restore the original state. */
XactReadOnly = false;
}
else
{
case CMD_SELECT:
command_is_ro = true;
break;
case CMD_INSERT:
case CMD_UPDATE:
case CMD_DELETE:
default:
command_is_ro = false;
break;
/* XactReadOnly is already false, nothing to restore. */
}
if (spock_readonly >= READONLY_USER && !command_is_ro)
ereport(ERROR, (errmsg("spock: invalid statement for a read-only cluster")));
}
Loading