firestore 在事务中的乐观锁定如何处理导致违反安全规则的并发更新?
How does firestore's optimistic locking in transactions handle concurrent updates that cause a violation of a security rule?
Firestore documentation 似乎没有指定何时或如何在事务中评估安全规则,以及它如何与重试和乐观锁定交互。
我的用例很简单,我的文档上有一个 lastUpdatedAt
字段,我正在使用事务来确保我获取最新文档并检查 lastUpdatedAt
字段,所以我可以在发布更新之前解决任何冲突。
在伪代码中,模式是这样的
async function saveDocument(data: MyType, docRef: DocumentReference)
await firebase.firestore().runTransaction(async (transaction) => {
const latestDocRef = await transaction.get(docRef)
const latestDoc = latestDocRef.data()
if(latestDoc.lastUpdatedAt > data.lastUpdatedAt){
data = resolveConflicts(data, latestDoc)
data.lastUpdatedAt = latestDoc.lastUpdatedAt
}
await transaction.update(docRef, data)
}
}
然后在安全规则中我检查以确保只允许更新最新或更新的 lastUpdatedAt
function hasNoLastUpdatedAtConflict(){
return (!("lastUpdatedAt" in resource.data) ||
resource.data.lastUpdatedAt == null ||
request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt);
}
//in individualRules
allow update: if hasNoLastUpdatedAtConflict() && someOtherConditionsEtc();
文档说
In the Mobile/Web SDKs, a transaction keeps track of all the documents you read inside the transaction. The transaction completes its write operations only if none of those documents changed during the transaction's execution. If any document did change, the transaction handler retries the transaction. If the transaction can't get a clean result after a few retries, the transaction fails due to data contention.
但是他们没有具体说明该行为如何与安全规则交互。我上面的交易因违反安全规则而失败 somtimes。它只在实时 Firestore 环境中失败,我无法在模拟器中使其失败。我怀疑发生的事情是:
- 交易开始,向客户发送文档
- 并发写入发生,它更改了 lastUpdatedAt
- 客户端看不到新的写入,因此无法解决冲突并按原样发布更新
- 安全规则现在因并发写入而失败,否则它会成功
- Firestore 使整个事务失败,但由于数据不干净而不是重试,权限被拒绝
我想我可以在交易因违反安全规则而被拒绝的情况下实现客户端重试,但如果确实发生了这种情况,那将是非常令人惊讶的行为。
是否有人了解安全规则和 Firestore 交易的实际行为与乐观锁定?
根据 documentation and code sample, you can ensure that related documents are always updated atomically and always as part of a transaction or batch write using the getAfter() 安全规则函数。它可用于在一组操作完成后但在 Cloud Firestore 提交操作之前访问和验证文档的状态。与 get() 一样,getAfter() 函数采用完全指定的文档路径。您可以使用 getAfter() 来定义必须作为事务或批处理一起发生的写入集。
这些是一些access call limits,您可能想看看。
文档表明 getAfter 可用于在记录整个事务的状态(在内存中的一种“暂存”环境中)之后检查数据库的内容,但在事务实际更改数据库之前,可见给大家。这与 get() 不同,因为 get() 仅在事务最终提交之前查看数据库的实际内容。简而言之,getAfter() 使用整个事务或批次的整个阶段写入,而 get 使用数据库实际存在的内容。
getAfter() 在您需要检查交易或批次中可能已更改的其他文档时很有用,并且仍然有机会因规则失败而拒绝整个交易或批次。因此,例如,如果在单个事务中写入的两个文档必须具有一些共同的字段值才能保持一致,则需要使用 getAfter() 来验证两者之间的相等性。
注意点:写入的安全规则在数据库中的任何内容被写入更改之前生效。这就是安全规则能够安全有效地拒绝访问的方式,而不必回滚已经发生的任何写入。
经过全面测试后,我确认使用 iOS/android 库中的乐观锁定的 firestore 事务可以被拒绝并出现 permission-denied 错误,因为在读取文档后发生并发写入交易。
出现以下情况:
- 开始交易
- 阅读文档,例如
{id: 1, lastUpdatedAt: 1, data: "foo"}
- 在事务中写入该文档之前,云 firestore 触发器将文档更新为
{id: 1, lastUpdatedAt: 2, data: "foo"}
- 更新交易文档
.update({lastUpdatedAt: 1, data: "bar"})
- 事务抛出异常
firestore/permission-denied
因为此更新违反了 request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt
的安全规则
事务确实没有按照文档建议重试,即使它执行了对陈旧数据的读取。这意味着如果并发写入可能导致事务违反安全规则,则 Firestore 库的用户不能依赖正在重试的事务。
这是令人惊讶且未记录的行为!
Firestore documentation 似乎没有指定何时或如何在事务中评估安全规则,以及它如何与重试和乐观锁定交互。
我的用例很简单,我的文档上有一个 lastUpdatedAt
字段,我正在使用事务来确保我获取最新文档并检查 lastUpdatedAt
字段,所以我可以在发布更新之前解决任何冲突。
在伪代码中,模式是这样的
async function saveDocument(data: MyType, docRef: DocumentReference)
await firebase.firestore().runTransaction(async (transaction) => {
const latestDocRef = await transaction.get(docRef)
const latestDoc = latestDocRef.data()
if(latestDoc.lastUpdatedAt > data.lastUpdatedAt){
data = resolveConflicts(data, latestDoc)
data.lastUpdatedAt = latestDoc.lastUpdatedAt
}
await transaction.update(docRef, data)
}
}
然后在安全规则中我检查以确保只允许更新最新或更新的 lastUpdatedAt
function hasNoLastUpdatedAtConflict(){
return (!("lastUpdatedAt" in resource.data) ||
resource.data.lastUpdatedAt == null ||
request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt);
}
//in individualRules
allow update: if hasNoLastUpdatedAtConflict() && someOtherConditionsEtc();
文档说
In the Mobile/Web SDKs, a transaction keeps track of all the documents you read inside the transaction. The transaction completes its write operations only if none of those documents changed during the transaction's execution. If any document did change, the transaction handler retries the transaction. If the transaction can't get a clean result after a few retries, the transaction fails due to data contention.
但是他们没有具体说明该行为如何与安全规则交互。我上面的交易因违反安全规则而失败 somtimes。它只在实时 Firestore 环境中失败,我无法在模拟器中使其失败。我怀疑发生的事情是:
- 交易开始,向客户发送文档
- 并发写入发生,它更改了 lastUpdatedAt
- 客户端看不到新的写入,因此无法解决冲突并按原样发布更新
- 安全规则现在因并发写入而失败,否则它会成功
- Firestore 使整个事务失败,但由于数据不干净而不是重试,权限被拒绝
我想我可以在交易因违反安全规则而被拒绝的情况下实现客户端重试,但如果确实发生了这种情况,那将是非常令人惊讶的行为。
是否有人了解安全规则和 Firestore 交易的实际行为与乐观锁定?
根据 documentation and code sample, you can ensure that related documents are always updated atomically and always as part of a transaction or batch write using the getAfter() 安全规则函数。它可用于在一组操作完成后但在 Cloud Firestore 提交操作之前访问和验证文档的状态。与 get() 一样,getAfter() 函数采用完全指定的文档路径。您可以使用 getAfter() 来定义必须作为事务或批处理一起发生的写入集。
这些是一些access call limits,您可能想看看。
文档表明 getAfter 可用于在记录整个事务的状态(在内存中的一种“暂存”环境中)之后检查数据库的内容,但在事务实际更改数据库之前,可见给大家。这与 get() 不同,因为 get() 仅在事务最终提交之前查看数据库的实际内容。简而言之,getAfter() 使用整个事务或批次的整个阶段写入,而 get 使用数据库实际存在的内容。
getAfter() 在您需要检查交易或批次中可能已更改的其他文档时很有用,并且仍然有机会因规则失败而拒绝整个交易或批次。因此,例如,如果在单个事务中写入的两个文档必须具有一些共同的字段值才能保持一致,则需要使用 getAfter() 来验证两者之间的相等性。
注意点:写入的安全规则在数据库中的任何内容被写入更改之前生效。这就是安全规则能够安全有效地拒绝访问的方式,而不必回滚已经发生的任何写入。
经过全面测试后,我确认使用 iOS/android 库中的乐观锁定的 firestore 事务可以被拒绝并出现 permission-denied 错误,因为在读取文档后发生并发写入交易。
出现以下情况:
- 开始交易
- 阅读文档,例如
{id: 1, lastUpdatedAt: 1, data: "foo"}
- 在事务中写入该文档之前,云 firestore 触发器将文档更新为
{id: 1, lastUpdatedAt: 2, data: "foo"}
- 更新交易文档
.update({lastUpdatedAt: 1, data: "bar"})
- 事务抛出异常
firestore/permission-denied
因为此更新违反了request.resource.data.lastUpdatedAt >= resource.data.lastUpdatedAt
的安全规则
事务确实没有按照文档建议重试,即使它执行了对陈旧数据的读取。这意味着如果并发写入可能导致事务违反安全规则,则 Firestore 库的用户不能依赖正在重试的事务。
这是令人惊讶且未记录的行为!