Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose default_transaction_... Postgres settings #8276

Merged
merged 10 commits into from
Feb 5, 2025
Merged

Expose default_transaction_... Postgres settings #8276

merged 10 commits into from
Feb 5, 2025

Conversation

elprans
Copy link
Member

@elprans elprans commented Jan 29, 2025

This exposes the default_transaction_isolation,
default_transaction_deferrable and default_transaction_read_only (as
default_transaction_access_mode) settings.

TODO:

  • make it so that default_transaction_isolation := ReadCommitted also makes the transaction read-only;
  • make sure transaction settings apply to the statement immediately after the SET command (possibly by issuing a corresponding SET transaction_XXX =).
  • fix interaction with query cache (currently cache will fail to persist if the default transaction mode is read-only (not fixed; worked around)
  • replace _dml_dummy with a temporary table otherwise _inject_config_cache_clear fails in a read-only transaction (and config set should work there)
  • issue: configure system set default_transaction_isolation followed by a reset doesn't fall back to connection default serializable in EDGEDB_SERVER_SETTINGS, instead it becomes PG default read committed. (nvm, this is \psql not applying the connection params)
  • emulate default_transaction_access_mode=ReadOnly for explicit transactions
  • emulate default_transaction_access_mode=ReadOnly for implicit transactions
  • emulate default_transaction_access_mode=ReadOnly for SQL transactions (the corresponding backend settings are already there, do we still need the frontend access_mode?)
  • fix explicit transaction read-only enforcement when effective isolation level is lower than Serializable
  • add test
  • sys::get_transaction_isolation() reports the wrong level if default_transaction_isolation is set on system/database level (because "postgres client" tier is higher than "postgres database" or "configuration file")

Base automatically changed from enum-config to master January 30, 2025 18:44
This exposes the `default_transaction_isolation`,
`default_transaction_deferrable` and `default_transaction_read_only` (as
`default_transaction_access_mode`) settings.
@fantix
Copy link
Member

fantix commented Jan 31, 2025

UPDATE: most examples in the second-half (original comment) of this comment are NOT how this PR behaves; the default_transaction_* settings actually behave similarly to the ones in Postgres:

  • default_transaction_* settings apply to implicit transactions (a.k.a. commands out of an explicit transaction with start transaction or equivalent client-binding interfaces)
  • start transaction will take transaction modifiers from the default_transaction_* settings, unless explicitly specified
  • When the effective default_transaction_isolation is lower than Serializable, the effective default_transaction_access_mode will always be ReadOnly; this applies to both implicit and explicit transactions.
  • Read-only implicit transactions are currently emulated in the Gel server to avoid internal complications - the underlying transactions are still read-write. That means the default_transaction_deferrable is not effective on implicit transactions; start an explicit transaction if you need it.

(original comment)

  • make it so that default_transaction_isolation := ReadCommitted also makes the transaction read-only;
  • make sure transaction settings apply to the statement immediately after the SET command (possibly by issuing a corresponding SET transaction_XXX =).

Postgres facts:

  • All Postgres commands are executed in a transaction, either explicit or implicit
  • All transactions have 3 properties: isolation/deferrable/read_only
  • Postgres has 2 sets of settings for each of the 3 properties: the transaction_* and the default_transaction_*
  • The default_transaction_* settings can be set on system/database/session levels, and follows the overriding rule
  • The transaction_* settings can only be set on the session level, and only reflects/affects the current transaction
  • transaction_isolation can only be changed as the first command in a transaction

Intermediate conclusions:

  • The values of transaction_* settings can be considered initialized whenever a new transaction starts, deriving from the override-resolved default_transaction_* settings
  • Within a transaction , the values of transaction_* settings no longer change as the corresponding default_transaction_* values change
  • So explicit transactions are always started as how the default_transaction_* settings were configured, and further configurable through the transaction_* settings from within the transaction (with the "isolation" exception)
  • Single-command implicit transactions are always controlled by the default_transaction_* settings
  • Multi-command implicit transactions are similar to explicit transactions

The Gel complication:

  • We only "expose" the default_transaction_* settings (with slightly different names/values) and - at the same time - want to apply the change of their values to both future and current transactions (excluding the "isolation" exception)
  • If the effective "isolation" level is lower than "serializable", "read_only" must be effectively "on" regardless of user settings
  • Resetting configs should apply the rules above, regardless of the config tier of system/database/session
  • Gel keeps a copy of the settings, which is usually the same as the effective backend values but we may have to hack the session values to make it work
  • Especially, the system settings is also stored as a comment on the system database __edgedbsys__ - modifying the comment requires non-read-only access

Complication examples:

  1. Current transaction affected by changing settings on any tier:
> start transaction;
OK: START TRANSACTION
[tx]> configure current database set default_transaction_access_mode := <sys::TransactionAccessMode>'ReadOnly';
OK: CONFIGURE DATABASE
[tx]> create type A;
gel error: TransactionError: cannot execute SELECT FOR UPDATE in a read-only transaction
  1. Current transaction NOT affected by changing settings on an overridden tier:
> configure session set default_transaction_access_mode := <sys::TransactionAccessMode>'ReadWrite';
OK: CONFIGURE SESSION
> start transaction;  # inherit the default_transaction_access_mode
OK: START TRANSACTION
[tx]> configure current database set default_transaction_access_mode := <sys::TransactionAccessMode>'ReadOnly';
OK: CONFIGURE DATABASE
[tx]> create type A;  # database tier is overridden by session tier
OK: CREATE TYPE
  1. The "isolation" exception:
> start transaction;
OK: START TRANSACTION
[tx]> select 42;  # pin the isolation level
{42}
[tx]> configure session set default_transaction_isolation := <sys::TransactionIsolation>'RepeatableRead';
OK: CONFIGURE SESSION
[tx]> select cfg::Config.default_transaction_isolation;  # the default config doesn't reflect the current transaction
{RepeatableRead}
[tx]> select cfg::Config.default_transaction_access_mode;  # read-only is enforced
{ReadOnly}
[tx]> create type A;  # but the current transaction is still Serializable+ReadWrite
OK: CREATE TYPE
  1. Derived "isolation" overwrites higher-tier "read_only":
> configure system set default_transaction_isolation := <sys::TransactionIsolation>'RepeatableRead';
OK: CONFIGURE INSTANCE
> select cfg::Config.default_transaction_access_mode;  # read-only is enforced
{ReadOnly}
> configure session set default_transaction_access_mode := <sys::TransactionAccessMode>'ReadWrite';
OK: CONFIGURE SESSION
> select cfg::Config.default_transaction_access_mode;  # read-only can be set, but not effective
{ReadWrite}
> create type A;
gel error: TransactionError: cannot execute SELECT FOR UPDATE in a read-only transaction
> configure current database set default_transaction_isolation := <sys::TransactionIsolation>'Serializable';
OK: CONFIGURE DATABASE
> create type A;  # unless the effective "isolation" allows it
OK: CREATE TYPE
  1. Resetting low-tier configs also affects the current transaction:
> configure current database set default_transaction_isolation := <sys::TransactionIsolation>'RepeatableRead';
OK: CONFIGURE DATABASE
> select cfg::Config.default_transaction_access_mode;  # read-only is enforced
{ReadOnly}
> start transaction;
OK: START TRANSACTION
[tx]> configure current database reset default_transaction_isolation;
OK: CONFIGURE DATABASE
[tx]> create type A;  # read-only is effectively cleared
OK: CREATE TYPE

@elprans
Copy link
Member Author

elprans commented Jan 31, 2025

  • We only "expose" the default_transaction_* settings (with slightly different names/values) and - at the same time - want to apply the change of their values to both future and current transactions (excluding the "isolation" exception)

We don't want default_transaction_isolation to apply to the current transaction, at least not in the sense of transaction_isolation, we just want to make sure it is applied to the next statement and because of the way we implement session config application it ends up lumped into the implicit transaction.

@fantix
Copy link
Member

fantix commented Feb 3, 2025

@elprans I pushed a few commits according to our recent PM discussion; I still don't feel this is right - like the fact that the actual access mode for implicit ReadOnly transactions is still ReadWrite underneath violates the description of default_transaction_deferrable (making it useless?):

... It currently has no effect on read-write transactions ...

Meanwhile, explicit transactions (if they're set to read-only because of default_transaction_access_mode) behave "correctly", which is inconsistent with how implicit transactions are emulated to be read-only.

@fantix fantix marked this pull request as ready for review February 4, 2025 20:15
@fantix fantix requested a review from msullivan February 4, 2025 20:15
@msullivan
Copy link
Member

@elprans I pushed a few commits according to our recent PM discussion; I still don't feel this is right - like the fact that the actual access mode for implicit ReadOnly transactions is still ReadWrite underneath violates the description of default_transaction_deferrable (making it useless?):

... It currently has no effect on read-write transactions ...

Meanwhile, explicit transactions (if they're set to read-only because of default_transaction_access_mode) behave "correctly", which is inconsistent with how implicit transactions are emulated to be read-only.

Did you resolve these concerns out-of-band or are they still outstanding?

@@ -489,7 +489,7 @@ def scan_check_ctes(
name='flag', val=pgast.BooleanConstant(val=True)
)],
relation=pgast.RelRangeVar(relation=pgast.Relation(
schemaname='edgedb', name='_dml_dummy')),
name='_dml_dummy')),
Copy link
Member

Choose a reason for hiding this comment

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

Do you think we should try to invalidate the persistent cache because of this change, or should we just instruct affected users to clear the cache manually?

(This is a more general question about miscompilation bugs and the persistent cache that I'm not sure we've ever properly figured out)

Copy link
Member

Choose a reason for hiding this comment

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

Oh true! One way that might work is to always "upgrade" the persistent cache automatically after server upgrade (by recompiling the cached queries) even if there are no compiler changes - if it's already in downtime maintenance, why not make the most of it and make sure the query cache is up to date. Let me draft a PR for that.

should we just instruct affected users to clear the cache manually?

I'd use that as the last resort when we failed to make the server fix itself (which is likely the case now, but let me fix that).

Copy link
Member

Choose a reason for hiding this comment

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

Is extending the downtime (possibly by a lot) better than a cache flush? It's not totally obvious to me.

Maybe it would be best to come up with an empty cache and then repopulate in the background?

Copy link
Member

@msullivan msullivan Feb 5, 2025

Choose a reason for hiding this comment

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

I don't consider cache recompilation to be a release blocker, by the way; it's been an outstanding issue through all of 5.0, so we can live with it a little longer.

(Fixing it is still good, though; but lower priority than getting this PR finalized and landed)

@fantix
Copy link
Member

fantix commented Feb 5, 2025

@elprans I pushed a few commits according to our recent PM discussion; I still don't feel this is right - like the fact that the actual access mode for implicit ReadOnly transactions is still ReadWrite underneath violates the description of default_transaction_deferrable (making it useless?):

... It currently has no effect on read-write transactions ...

Meanwhile, explicit transactions (if they're set to read-only because of default_transaction_access_mode) behave "correctly", which is inconsistent with how implicit transactions are emulated to be read-only.

Did you resolve these concerns out-of-band or are they still outstanding?

Yeah, I just talked to Elvis; the use case of deferrables is mostly seen in explicit transactions, so this consistency is okay for now. I'll also double check that the cache persistence works okay.

@fantix
Copy link
Member

fantix commented Feb 5, 2025

The default_transaction_isolation currently doesn't work on system/database level due to the serializable value set in backend connection parameters; the fix is non-trivial and I'll start a new PR for it.

@msullivan
Copy link
Member

The default_transaction_isolation currently doesn't work on system/database level due to the serializable value set in backend connection parameters; the fix is non-trivial and I'll start a new PR for it.

Setting it on the system/database level is kind of marginal anyway, though, since serializable is so limited.
It wouldn't be terrible if it we only supported it on sessions.

@fantix fantix merged commit 300b877 into master Feb 5, 2025
25 checks passed
@fantix fantix deleted the tx-config branch February 5, 2025 23:48
@fantix fantix added the to-backport-6.x PRs that *should* be backported to 6.x label Feb 5, 2025
msullivan pushed a commit that referenced this pull request Feb 5, 2025
* `default_transaction_*` settings apply to implicit transactions
* `start transaction` will take transaction modifiers from the
  `default_transaction_*` settings, unless explicitly specified
* When the effective `default_transaction_isolation` is lower than
  `Serializable`, the effective `default_transaction_access_mode`
  will always be `ReadOnly`
* Read-only implicit transactions are currently emulated in
  the Gel server to avoid internal complications
* `default_transaction_isolation` currently doesn't work on system/
  database level
msullivan added a commit that referenced this pull request Feb 6, 2025
* `default_transaction_*` settings apply to implicit transactions
* `start transaction` will take transaction modifiers from the
  `default_transaction_*` settings, unless explicitly specified
* When the effective `default_transaction_isolation` is lower than
  `Serializable`, the effective `default_transaction_access_mode`
  will always be `ReadOnly`
* Read-only implicit transactions are currently emulated in
  the Gel server to avoid internal complications
* `default_transaction_isolation` currently doesn't work on system/
  database level

Co-authored-by: Elvis Pranskevichus <[email protected]>
@msullivan msullivan added backported-6.x PRs that *have* been backported to 6.x and removed to-backport-6.x PRs that *should* be backported to 6.x labels Feb 6, 2025
msullivan added a commit that referenced this pull request Feb 7, 2025
fantix added a commit that referenced this pull request Feb 13, 2025
msullivan pushed a commit that referenced this pull request Feb 14, 2025
deepbuzin pushed a commit that referenced this pull request Feb 18, 2025
* `default_transaction_*` settings apply to implicit transactions
* `start transaction` will take transaction modifiers from the 
  `default_transaction_*` settings, unless explicitly specified
* When the effective `default_transaction_isolation` is lower than
  `Serializable`, the effective `default_transaction_access_mode`
  will always be `ReadOnly`
* Read-only implicit transactions are currently emulated in
  the Gel server to avoid internal complications
* `default_transaction_isolation` currently doesn't work on system/
  database level
deepbuzin pushed a commit that referenced this pull request Feb 18, 2025
fantix added a commit that referenced this pull request Feb 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backported-6.x PRs that *have* been backported to 6.x
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants