元组在授予权限时同时被更新

huangapple go评论69阅读模式
英文:

Tuple concurrently updated when granting permissions

问题

正在处理数据库查询问题 - 并非数据库专家,希望能得到帮助。

在动态创建数据库和模式时,偶尔会遇到以下错误:

无法应用数据库授权。
io.vertx.core.impl.NoStackTraceThrowable: Error granting permission.
io.vertx.pgclient.PgException:
ERROR: tuple concurrently updated (XX000)

在查询字符串中,角色名称、数据库名称和模式名称在另一个地方被替换,我修改了代码,直接将查询字符串传递到事务中以简化操作。

授予的权限如下:

private static final String ERR_PERMISSION_GRANT_ERROR_MESSAGE = "Error granting permission. ";
private static final String ADVISORY_LOCK = "SELECT pg_try_advisory_lock("
		+ String.valueOf(BigInteger.valueOf(Double.valueOf(Math.random()).longValue())) + ")";
private static final String CREATE_USER = "CREATE ROLE <role-name> LOGIN PASSWORD <pwd>;";
private static final String GRANT_PERMISSION1 = "GRANT CREATE, CONNECT ON DATABASE <db-name> TO <role-name>;";
private static final String GRANT_PERMISSION2 = "GRANT USAGE ON SCHEMA <schema-name> TO <role-name>;";
private static final String GRANT_PERMISSION3 = "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA <schema-name> TO <role-name>;";
private static final String GRANT_PERMISSION5 = "ALTER DEFAULT PRIVILEGES IN SCHEMA <schema-name> GRANT ALL ON SEQUENCES TO <role-name>;";

private static Promise<Boolean> grantDatabase(PgPool pool, String databaseName, String userName, String schemaName,
		Vertx vertx) {
	Promise<Boolean> promise = Promise.promise();

	pool.getConnection()
			// 事务必须使用连接
			.onSuccess(conn -> {
				// 开始事务
				conn.begin().compose(tx -> conn
						// 各种语句
						.query(updateQueryString(ADVISORY_LOCK, databaseName, userName)).execute()
						.compose(
								res1 -> conn.query(
										updateQueryString(GRANT_PERMISSION1, databaseName, userName))
										.execute()
										.compose(res2 -> conn.query(
												updateQueryString(GRANT_PERMISSION2, schemaName, userName))
												.execute()
												.compose(res3 -> conn
														.query(updateQueryString(
																GRANT_PERMISSION3, schemaName, userName))
														.execute()
														.compose(res4 -> conn
																.query(updateQueryString(GRANT_PERMISSION5,
																		schemaName, userName))
																.execute()))))
						// 提交事务
						.compose(res5 -> tx.commit()))
						// 将连接返回到池中
						.eventually(v -> conn.close()).onSuccess(v -> promise.complete(Boolean.TRUE))
						.onFailure(err -> promise
								.fail(ERR_PERMISSION_GRANT_ERROR_MESSAGE
			));
	return promise;
}

在这种情况下,我该如何修复"tuple concurrently updated"错误?我的服务只有一个实例在运行。

PostgreSQL v14.6 (Homebrew)
vertx-pg-client 4.3.8

英文:

Struggling with database queries - not a db expert by any means, any help would be appreciated.

When dynamically created databases and schemas, once in awhile I get this error:

> Unable to apply database grants.
> io.vertx.core.impl.NoStackTraceThrowable: Error granting permission.
> io.vertx.pgclient.PgException:
> ERROR: tuple concurrently updated (XX000)

The role names, database names and schema names are replaced in the query strings in a separate place, i modified the code to pass in the query string directly to the transaction for simplicity.

The permissions being granted are as follows:

private static final String ERR_PERMISSION_GRANT_ERROR_MESSAGE = &quot;Error granting permission. &quot;;
private static final String ADVISORY_LOCK = &quot;SELECT pg_try_advisory_lock(&quot;
		+ String.valueOf(BigInteger.valueOf(Double.valueOf(Math.random()).longValue())) + &quot;)&quot;;  
private static final String CREATE_USER = &quot;CREATE ROLE &lt;role-name&gt; LOGIN PASSWORD &lt;pwd&gt;;&quot;;
private static final String GRANT_PERMISSION1 = &quot;GRANT CREATE, CONNECT ON DATABASE &lt;db-name&gt; TO &lt;role-name&gt;;&quot;;
private static final String GRANT_PERMISSION2 = &quot;GRANT USAGE ON SCHEMA &lt;schema-name&gt; TO &lt;role-name&gt;;&quot;;
private static final String GRANT_PERMISSION3 = &quot;GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA &lt;schema-name&gt; TO &lt;role-name&gt;&quot;;
private static final String GRANT_PERMISSION5 = &quot;ALTER DEFAULT PRIVILEGES IN SCHEMA &lt;schema-name&gt; GRANT ALL ON SEQUENCES TO &lt;role-name&gt;;&quot;;


private static Promise&lt;Boolean&gt; grantDatabase(PgPool pool, String databaseName, String userName, String schemaName,
		Vertx vertx) {
	Promise&lt;Boolean&gt; promise = Promise.promise();

	pool.getConnection()
			// Transaction must use a connection
			.onSuccess(conn -&gt; {
				// Begin the transaction
				conn.begin().compose(tx -&gt; conn
						// Various statements
						.query(updateQueryString(ADVISORY_LOCK, databaseName, userName)).execute()
						.compose(
								res1 -&gt; conn.query(
										updateQueryString(GRANT_PERMISSION1 databaseName, userName))
										.execute()
										.compose(res2 -&gt; conn.query(
												updateQueryString(GRANT_PERMISSION2, schemaName, userName))
												.execute()
												.compose(res3 -&gt; conn
														.query(updateQueryString(
																GRANT_PERMISSION3, schemaName, userName))
														.execute()
														.compose(res4 -&gt; conn
																.query(updateQueryString(GRANT_PERMISSION5,
																		schemaName, userName))
																.execute()))))
						// Commit the transaction
						.compose(res5 -&gt; tx.commit()))
						// Return the connection to the pool
						.eventually(v -&gt; conn.close()).onSuccess(v -&gt; promise.complete(Boolean.TRUE))
						.onFailure(err -&gt; promise
								.fail(ERR_PERMISSION_GRANT_ERROR_MESSAGE
			});
	return promise;
}

How do I fix the tuple concurrently updated error in this case? I only have a single instance of my service running.

PostgreSQL v14.6 (Homebrew)
vertx-pg-client 4.3.8

答案1

得分: 0

您可能已经发现这个链接,并确定错误是由于两个线程在同时运行这些查询的机会不为零而引起的。它们也可能与其他某些内容竞争 - 缺少逗号和括号暗示您所显示的代码不是与您正在运行的代码一一对应的,而且您可能还有更多的授权/取消授权/修改操作在其他地方。

我认为您计划使用咨询锁比建立单独的“授权队列”或尝试跟踪和锁定系统表的替代方法更好。

锁定方法

private static final String ADVISORY_LOCK = "SELECT pg_try_advisory_lock("
/* ... */
.query(updateQueryString(ADVISORY_LOCK, databaseName, userName)).execute()
                        .compose( /* ... */

您可能希望更改您的咨询锁函数

  • pg_advisory_lock()会使其等待锁可用。

  • pg_try_advisory_lock()不会使客户端等待锁可用,而是返回false。我没有看到代码以任何方式响应truefalse的结果,如果它获得了锁或没有获得锁,则它只是尝试获取锁并忽略结果,无论如何继续进行。

  • 以上两种方式都会获得会话级锁,因此除非您在相同的ID上调用pg_advisory_unlock(),否则不会被释放。从pg_advisory_xact_lock()pg_try_advisory_lock()获得的锁将在提交/回滚时自动释放。

    对于独立连接,conn.close()应该结束会话,触发数据库取消会话级和事务级锁。对于连接池,除非它恰好被配置为进行清理,否则它可能在释放后继续存在,仍然保持锁定。

用于锁定的ID
您使用的Math.random()似乎总是会因为缩小原始转换而导致0,在Double.longValue()

String.valueOf(         //BigInt to String
   BigInteger.valueOf(  //Long   to BigInt, 
      Double.valueOf(   //Double to Double
          Math.random() //返回0.0到1.0之间的值
      ).longValue()     //Double to Long,基本上是将其舍为0
   )
)

这意味着您已经始终在重复使用一个静态ID。

但如果您尝试随机化ID以使每个线程使用不同的唯一锁定ID,它们将无法阻塞彼此。线程需要使用相同的锁定ID来引用相同的“操作”,如果它们同时尝试执行该操作,它们可能会互相干扰。

private static final String ADVISORY_LOCK = "SELECT pg_try_advisory_lock("
  + String.valueOf(BigInteger.valueOf(Double.valueOf(Math.random()).longValue()))
  + ")"; 
--生成的随机锁ID为99:
/*第1个代理:*/ SELECT pg_try_advisory_lock(99);
--获得锁,第1个代理继续运行其查询

--同时,第2个代理获得了一个随机ID为77:
/*第2个代理:*/ SELECT pg_try_advisory_lock(77);
--77没有被锁定,因此它立即尝试执行与第1个代理相同的操作,而不考虑它们之间的竞争,这可能导致`ERROR: tuple concurrently updated`

除了将pg_try_advisory_lock()替换为pg_advisory_xact_lock(),我认为将Math.random()替换为静态的任意数字就足够了:

private static final String ADVISORY_LOCK = "SELECT pg_advisory_xact_lock("
        + "123456789"
        + ")"; 
--现在,每个尝试运行这些特定查询的人都检查相同的ID
/*第1个代理:*/ SELECT pg_advisory_xact_lock(123456789);
--迄今为止还没有人调用该ID,所以允许它继续进行

--同时,第2个代理进入相同的子例程并询问相同的ID:
/*第2个代理:*/ SELECT pg_advisory_xact_lock(123456789);
--第1个代理尚未释放该ID上的锁,所以第2个代理等待

如果您的应用程序的竞争部分正在初始化具有相同共享种子的自己的Random(),或重新启动一个共享的Random(),它们将获得相同的ID - 但这只是将预定义的静态ID替换为预定义的种子。

随机的唯一锁ID可能有助于避免某些不相关操作的意外ID重用,并使您无需跟踪ID在何处使用。但这些ID必须在运行时之前或在每次初始化期间生成。

英文:

You've probably already found this and established the error is caused by the non-zero chance of two of your threads trying to run those queries at the same time. They could be also competing with something else - missing commas and parentheses suggest the code you showed is not 1:1 what you're running, plus you could have more grant/revoke/alter elsewhere.

I think your plan to use advisory locks is better than the alternative of establishing a separate "grant queue" or trying to track and lock system tables.


Locking approach

private static final String ADVISORY_LOCK = &quot;SELECT pg_try_advisory_lock(&quot; 
/* ... */
.query(updateQueryString(ADVISORY_LOCK, databaseName, userName)).execute()
                        .compose( /* ... */

You might want to change your advisory lock function.

  • pg_advisory_lock() would make it wait for the lock if it's not available.

  • pg_try_advisory_lock() instead of making the client wait for lock to become available, returns false. I don't see the code in any way responding to the result of true if it got the lock or false if it didn't, which means that it just tries to acquire the lock and ignores the outcome, continuing regardless.

  • Both of the above obtain a session-level lock, so it won't be released unless you call pg_advisory_unlock() on the same ID. Lock obtained from pg_advisory_xact_lock() and pg_try_advisory_lock() would be released automatically at commit/rollback.

    With a standalone connection, conn.close() should end the session which triggers the db to lift both session- and transaction-level locks it held. With a pool, it could live on after released, still holding the locks unless it happens to get cleaned up by a pool configured to do so.


ID used for locking

Your use of Math.random() seems to always result in a 0 because of narrowing primitive conversion in Double.longValue()

String.valueOf(         //BigInt to String
   BigInteger.valueOf(  //Long   to BigInt, 
      Double.valueOf(   //Double to Double
          Math.random() //returns between 0.0 and 1.0
      ).longValue()     //Double to Long, basically flooring it to 0
   )
)

Which means you're already always re-using a static ID.

But in case you tried to randomise the ID to make each thread use a different, unique lock id, they wouldn't be able to block each other. Threads need to use the same lock ID in reference to the same "action" that could interfere with the other threads if they attempted it at the same time.

private static final String ADVISORY_LOCK = &quot;SELECT pg_try_advisory_lock(&quot;
  + String.valueOf(BigInteger.valueOf(Double.valueOf(Math.random()).longValue()))
  + &quot;)&quot;; 
--Random lock ID generated as 99:
/*1st agent:*/ SELECT pg_try_advisory_lock(99);
--lock acquired, 1st agent proceeds to run its queries

--In parallel, 2nd agent gets a random ID of 77:
/*2nd agent:*/ SELECT pg_try_advisory_lock(77);
--77 wasn&#39;t locked, so it immediately proceeds to attempt the same action 
--as the 1st agent, disregarding the fact that it can make them compete
--and result in `ERROR: tuple concurrently updated`

Aside from swapping pg_try_advisory_lock() for a pg_advisory_xact_lock() I think replacing that Math.random() with a static, arbitrary number, will be enough:

private static final String ADVISORY_LOCK = &quot;SELECT pg_advisory_xact_lock(&quot;
        + &quot;123456789&quot;
        + &quot;)&quot;; 
--now everyone trying to run those particular queries checks the same ID
/*1st agent:*/ SELECT pg_advisory_xact_lock(123456789);
--noone called dibs on that ID so far, so it&#39;s allowed to proceed

--In parallel, 2nd agent enters the same subroutine and asks about the same ID:
/*2nd agent:*/ SELECT pg_advisory_xact_lock(123456789);
--1st agent hasn&#39;t released the lock on that ID yet, so 2nd agent waits

If competing parts of your app were initialising their own Random() with the same, shared seed, or re-starting a shared Random(), they'd get the same ID - but that's only trading a predefined, static ID for a predefined seed.

Random, unique lock IDs could be useful to avoid accidental ID re-use for some unrelated action and to free you from having to keep track of what ID was used where. However, those IDs would have to be generated ahead of runtime or during each initialisation.

huangapple
  • 本文由 发表于 2023年2月9日 02:52:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75390456.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定