Skip to content
Closed
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
38 changes: 27 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,25 @@ make USE_PGXS=1
make USE_PGXS=1 install
```

After installing the lolor extension, connect to your Postgres database and create the extension with the command:
After installing the lolor extension, add it to `shared_preload_libraries` and restart the server (see [Configuring lolor](#configuring-lolor)), then connect to your Postgres database and create the extension with the command:

```
CREATE EXTENSION lolor;
```

### Configuring lolor

You must set the `lolor.node` parameter before using the extension. The value can be from 1 to 2^28; the value is used to help in generation of new large object OID.
lolor must be loaded at server start. Add it to `shared_preload_libraries` in
`postgresql.conf` and restart; `CREATE EXTENSION lolor` and every `lo_*` call
refuse when the library was loaded on demand. The guard that protects large
objects when the extension is dropped is an object access hook, which is only
in place in every backend when the library is preloaded.

```
shared_preload_libraries = 'lolor'
```

You must set the `lolor.node` parameter before using the extension. The value can be from 1 to 15 (0 means unset); it is encoded in the four low bits of every large object OID lolor generates, so each node must use a different value.

```
lolor.node = 1
Expand All @@ -70,8 +80,8 @@ SELECT spock.repset_add_table('spock_replication_set', 'lolor.pg_largeobject_met

### Migrating large objects

Migration from native to lolor is **manual**; migration back is **automatic**
on `DROP EXTENSION` so no objects are ever lost.
Migration is a manual step in both directions. Dropping the extension never
moves data: it is refused while any object remains in lolor storage.

Migrate existing native large objects into lolor storage (requires superuser):

Expand All @@ -80,14 +90,20 @@ CREATE EXTENSION lolor;
SELECT lolor.migrate_from_native();
```

Reverse migration happens automatically when the extension is dropped, or can
be triggered manually:
To remove the extension, migrate the objects back first:

```sql
SELECT lolor.migrate_to_native(); -- manual
DROP EXTENSION lolor; -- automatic
SELECT lolor.migrate_to_native();
DROP EXTENSION lolor;
```

An object access hook refuses to remove the extension while it is enabled or
while any object remains in lolor storage, on every path that reaches it
(`DROP EXTENSION`, `DROP SCHEMA lolor CASCADE`, `DROP OWNED BY`). The drop
itself only puts the native `pg_catalog.lo_*` names back. To discard whatever
is in lolor storage instead, a superuser can set `lolor.allow_unsafe_drop = on`
for the session that runs the drop; it skips both checks.

Both directions preserve original OIDs, owners, ACLs, comments and data, and
copy pages verbatim so sparse large objects stay sparse. Moving an object to
native storage also reinstates its `pg_shdepend` entries, so `DROP ROLE`,
Expand Down Expand Up @@ -143,7 +159,7 @@ database user. Upgrading to 1.3.0 revokes the privilege; see the release notes.

- Native large object functionality cannot be used while you are using the lolor extension.
- lolor does not support the following statements against objects held in lolor storage: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`. Owners, ACLs and comments set while an object was in native storage are preserved across migration in both directions.
- `lolor.enable()`, `lolor.disable()` and the migration functions refuse while any other session is connected to the database, the same rule as `ALTER DATABASE ... RENAME`: a renamed function keeps its OID, so a session that already resolved the `lo_*` functions keeps calling the previous implementation, and the row movement cannot be made atomic for other backends either. The calling session must reconnect after `enable()` or `disable()`.
- Objects in lolor storage are rows in ordinary tables and so cannot participate in `pg_shdepend`. `DROP ROLE`, `DROP OWNED BY` and `REASSIGN OWNED BY` do not see them: a role that owns them or appears in their ACL can be dropped without a warning, and `REASSIGN OWNED` / `DROP OWNED` leave them untouched. Before dropping a role, run `REASSIGN OWNED BY` or `DROP OWNED BY` in each database that has lolor, then `DROP ROLE`. Afterwards run `lolor.check_orphans()` in each such database and repair anything it reports with `lolor.fix_orphans(new_owner)`; `lolor.migrate_to_native()` refuses while orphans exist, and since `DROP EXTENSION` runs that migration, so does the drop.
- `lolor.enable()`, `lolor.disable()` and the migration functions refuse while any other session is connected to the database, the same rule as `ALTER DATABASE ... RENAME`: a renamed function keeps its OID, so a session that already resolved the `lo_*` functions keeps calling the previous implementation, and the row movement cannot be made atomic for other backends either. The calling session must reconnect after `enable()` or `disable()`. They also refuse to run from anything but a client session, so replicated DDL cannot drive them from an apply worker.
- Objects in lolor storage are rows in ordinary tables and so cannot participate in `pg_shdepend`. `DROP ROLE`, `DROP OWNED BY` and `REASSIGN OWNED BY` do not see them: a role that owns them or appears in their ACL can be dropped without a warning, and `REASSIGN OWNED` / `DROP OWNED` leave them untouched. Before dropping a role, run `REASSIGN OWNED BY` or `DROP OWNED BY` in each database that has lolor, then `DROP ROLE`. Afterwards run `lolor.check_orphans()` in each such database and repair anything it reports with `lolor.fix_orphans(new_owner)`; `lolor.migrate_to_native()` refuses while orphans exist.
- Role OIDs come from a cluster-wide counter that wraps around, so a new role can receive a dropped role's OID and silently become the owner or grantee of that role's orphaned objects. This cannot be detected after the fact, which is another reason to run `lolor.check_orphans()` promptly after dropping roles.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects, connecting to each node directly. Do not send it through DDL replication: that runs the migration inside every subscriber's apply worker, where a refusal (an OID conflict, a security label) leaves that node's replication retrying forever, and where a node can receive another node's post-migration changes before it has migrated itself. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows. To make that hazard an error rather than silent divergence, collect the other nodes' OIDs with `lolor.native_lo_oids()` and pass them in: `SELECT lolor.migrate_from_native(peer_oids => ARRAY[...])` refuses when any of them collide. After migrating every node, compare `lolor.digest()` across nodes to confirm they converged.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects, connecting to each node directly. It refuses to run inside an apply worker, so sending it through DDL replication fails on every subscriber. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows. To make that hazard an error rather than silent divergence, collect the other nodes' OIDs with `lolor.native_lo_oids()` and pass them in: `SELECT lolor.migrate_from_native(peer_oids => ARRAY[...])` refuses when any of them collide. After migrating every node, compare `lolor.digest()` across nodes to confirm they converged.
7 changes: 7 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ cd /tmp/lolor-build
make USE_PGXS=1 with_llvm=no
make USE_PGXS=1 with_llvm=no install

# lolor refuses to load on demand, and the library did not exist when the
# server above was started. Preload it from here on; the later setting wins.
cat >> "$PGDATA/postgresql.conf" <<_EOF_
shared_preload_libraries = 'spock, lolor'
_EOF_
pg_ctl -D "$PGDATA" -l /home/pgedge/logfile.log -o "-k /tmp" -m fast -w restart

psql -U admin -d demo -h /tmp -v ON_ERROR_STOP=1 <<_EOF_
create extension lolor;
alter system set lolor.node to ${HOSTNAME: -1};
Expand Down
46 changes: 32 additions & 14 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ Use of the `lolor` extension requires Postgres 16 or newer.

## Migrating large objects

Migration from native to lolor storage is **manual** — you decide when to move
existing large objects. Migration back to native is **automatic** — dropping
the extension moves all objects back so nothing is lost.
Migration is a manual step in both directions: you decide when to move
existing large objects into lolor storage, and you move them back before
dropping the extension. Dropping the extension never moves data: it is
refused while any object remains in lolor storage.

### Native to lolor (manual)

Expand All @@ -39,25 +40,39 @@ when there are no native large objects. This step is intentionally not
automatic: it requires superuser privileges and should be performed during a
maintenance window.

### Lolor to native (automatic on DROP EXTENSION)
### Lolor to native (before DROP EXTENSION)

Large objects are automatically migrated back to native Postgres storage when the
extension is dropped:
Move the large objects back to native Postgres storage, then drop the
extension:

```sql
SELECT lolor.migrate_to_native();
DROP EXTENSION lolor;
```

This ensures that no large objects are lost if the extension is removed. You
can also trigger the reverse migration manually while the extension is still
installed:
Both paths preserve OIDs, owners, ACLs, comments and data, and copy pages
verbatim so sparse large objects stay sparse.

An object access hook refuses to remove the extension while it is enabled or
while any object remains in lolor storage, on every path that reaches it
(`DROP EXTENSION`, `DROP SCHEMA lolor CASCADE`, `DROP OWNED BY`). The drop
itself only puts the native `pg_catalog.lo_*` names back. To discard whatever
is in lolor storage instead, a superuser can set `lolor.allow_unsafe_drop = on`
for the session that runs the drop; it skips both checks.

`lolor.enable()`, `lolor.disable()` and both migration functions refuse while
any other session is connected to the database, and refuse to run from
anything but a client session. Reconnect client sessions after `enable()` or
`disable()`: libpq caches the large object function OIDs per connection.

Two helpers support verifying a migration:

```sql
SELECT lolor.migrate_to_native();
SELECT * FROM lolor.digest(); -- per-object checksum, compare across nodes
SELECT * FROM lolor.check_orphans(); -- objects referring to a dropped role
SELECT lolor.fix_orphans('some_role'); -- reassign them and drop dead ACL entries
```

Both paths preserve OIDs, owners, ACLs, and data.

When the spock extension is installed, both migration functions run under
`spock.repair_mode()`, so the row-shuffling migration DML is **not** replicated
to other nodes. This is essential for `migrate_to_native()`: its deletes from
Expand All @@ -82,5 +97,8 @@ retains the changes and delivers them when replication resumes.
## Limitations

- Native Postgres large object functionality cannot be used while you are using the lolor extension.
- lolor does not support the following statements: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects — for example with `spock.replicate_ddl('SELECT lolor.migrate_from_native()')`, which queues the command so that each node executes it locally. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows.
- lolor does not support the following statements against objects held in lolor storage: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`. Owners, ACLs and comments set while an object was in native storage are preserved across migration in both directions.
- `lolor.enable()`, `lolor.disable()` and the migration functions refuse while any other session is connected to the database, the same rule as `ALTER DATABASE ... RENAME`: a renamed function keeps its OID, so a session that already resolved the `lo_*` functions keeps calling the previous implementation, and the row movement cannot be made atomic for other backends either. The calling session must reconnect after `enable()` or `disable()`. They also refuse to run from anything but a client session, so replicated DDL cannot drive them from an apply worker.
- Objects in lolor storage are rows in ordinary tables and so cannot participate in `pg_shdepend`. `DROP ROLE`, `DROP OWNED BY` and `REASSIGN OWNED BY` do not see them: a role that owns them or appears in their ACL can be dropped without a warning, and `REASSIGN OWNED` / `DROP OWNED` leave them untouched. Before dropping a role, run `REASSIGN OWNED BY` or `DROP OWNED BY` in each database that has lolor, then `DROP ROLE`. Afterwards run `lolor.check_orphans()` in each such database and repair anything it reports with `lolor.fix_orphans(new_owner)`; `lolor.migrate_to_native()` refuses while orphans exist.
- Role OIDs come from a cluster-wide counter that wraps around, so a new role can receive a dropped role's OID and silently become the owner or grantee of that role's orphaned objects. This cannot be detected after the fact, which is another reason to run `lolor.check_orphans()` promptly after dropping roles.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects, connecting to each node directly. It refuses to run inside an apply worker, so sending it through DDL replication fails on every subscriber. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows. To make that hazard an error rather than silent divergence, collect the other nodes' OIDs with `lolor.native_lo_oids()` and pass them in: `SELECT lolor.migrate_from_native(peer_oids => ARRAY[...])` refuses when any of them collide. After migrating every node, compare `lolor.digest()` across nodes to confirm they converged.
10 changes: 8 additions & 2 deletions docs/install_configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ make USE_PGXS=1
make USE_PGXS=1 install
```

After the lolor extension is installed, connect to your Postgres database and create the extension with the command:
lolor must be loaded at server start. Add it to `shared_preload_libraries` in `postgresql.conf` and restart; `CREATE EXTENSION lolor` and every `lo_*` call refuse when the library was loaded on demand. The guard that protects large objects when the extension is dropped is an object access hook, which is only in place in every backend when the library is preloaded.

```
shared_preload_libraries = 'lolor'
```

Then connect to your Postgres database and create the extension with the command:

```
CREATE EXTENSION lolor;
```

## Configuring lolor

You must set the `lolor.node` parameter on each node in your replication cluster before using the extension. The value can be from 1 to 2^28; the value is used to help in generation of new large object OID.
You must set the `lolor.node` parameter on each node in your replication cluster before using the extension. The value can be from 1 to 15 (0 means unset); it is encoded in the four low bits of every large object OID lolor generates, so each node must use a different value.

```
lolor.node = 1
Expand Down
Loading
Loading