简体   繁体   English

如何使用 Firebase Firestore 获取悲观锁?

[英]How can I use Firebase Firestore to acquire a pessimistic lock?

I have some firebase function which does the following我有一些 firebase function 执行以下操作

  1. Accepts a partial payload of some record as input接受一些记录的部分有效载荷作为输入
  2. Reads that record from a third party从第三方读取该记录
  3. Patches that record with the partial payload and stores it on the third party使用部分有效载荷记录并将其存储在第三方的补丁

The third party has their own database and doesn't support any kind of partial updates / locking.第三方有自己的数据库,不支持任何类型的部分更新/锁定。 Users are likely to be calling this endpoint concurrently so I need to implement a pessimistic lock / FIFO queue to ensure the function never runs concurrently for that specific record.用户可能会同时调用此端点,因此我需要实施悲观锁/FIFO 队列以确保 function 永远不会针对该特定记录同时运行。 Otherwise, one user's update could clobber another.否则,一个用户的更新可能会破坏另一个用户。 Note, this is not a performance concern because it's very unlikely to have more than 10 concurrent calls.请注意,这不是性能问题,因为并发调用不太可能超过 10 个。

Here is how I imagine I could implement this locking in high level terms by adding step 0 and 4:以下是我想象的如何通过添加步骤 0 和 4 以高级术语实现此锁定:

0: Acquire a lock with a specific ID, if that lock is already acquired keep polling until it's free or the lock expires 0:获取具有特定 ID 的锁,如果该锁已被获取,则继续轮询直到它空闲或锁过期

... ...

4: Release the lock now that the record patch has completed 4:现在记录补丁已经完成释放锁

How could I implement such a locking mechanism with Firebase Firstore?我如何使用 Firebase Firstore 实现这样的锁定机制?

I found a way, you can use firebase transactions to check if a row exists or not, if it does not exists then you can acquire the lock:我找到了一个方法,你可以使用 firebase 事务来检查一行是否存在,如果不存在则可以获取锁:

import { getFirestore } from 'firebase-admin/firestore'

export type Lock = {
  lockId: string
  release: () => Promise<void>
}

export const Locks = {
  /** Use firestore to acquire a lock, by writing the expiration time to the `locks/${lockId}` document.
   * If the document already exists and the expiration time is in the future, then exit without acquiring the lock.
   * If the document does not exist or the expiration time is in the past, then the acquire the lock and return a Lock object. */
  async acquire(lockId: string, { lockDurationMs = 1000 * 60 } = {}): Promise<Lock | undefined> {
    const db = getFirestore()
    const docRef = db.collection('locks').doc(lockId)
    const maybeLock = await db.runTransaction(async transaction => {
      const doc = await transaction.get(docRef)
      const expiration = doc.data()?.expiration || 0
      if (expiration > Date.now()) {
        // Someone else has the lock
        return
      }
      const newExpiration = Date.now() + lockDurationMs
      await transaction.set(docRef, { expiration: newExpiration }, { merge: true })
      const lock: Lock = {
        lockId,
        release: async () => {
          await docRef.set({ expiration: null }, { merge: true })
        },
      }
      return lock
    })
    return maybeLock
  },
  /** Poll for a lock to be acquired, and return it when it is.
   * Will return undefined if the lock is not acquired within the timeout. */
  async pollUntilAcquired(
    lockId: string,
    { lockDurationMs = 1000 * 60, pollIntervalMs = 1000, timeoutMs = 1000 * 60 * 2 } = {}
  ) {
    const lock = await pollFor(() => this.acquire(lockId, { lockDurationMs }), {
      interval: pollIntervalMs,
      // If we don't get the lock within 2 minutes, then something is wrong
      timeout: timeoutMs,
    })
    return lock
  },
}

// #region Tests
if (process.env.TESTING) {
  async function testLock() {
    const { initFirebaseAdmin } = require('./initFirebaseAdmin')
    initFirebaseAdmin('staging')

    const db = getFirestore()
    const locks = db.collection('locks')

    async function getTestLock() {
      const doc = await locks.doc('testLock').get()
      const data = doc.data()
      return data?.expiration || 0
    }

    {
      // TEST 1: Acquire a lock, check it inserts the row, then release it and check it deletes the row
      const lock = await Locks.acquire('testLock', { lockDurationMs: 1000 })
      const expiration = await getTestLock()
      assert(expiration > Date.now(), 'Lock was not acquired')
      await lock?.release()
      const expiration2 = await getTestLock()
      assert(expiration2 === 0, 'Lock was not released')
    }

    {
      // TEST 2: Acquire a lock, then try to acquire it again and check it fails
      const lock = await Locks.acquire('testLock', { lockDurationMs: 1000 })
      const lock2 = await Locks.acquire('testLock', { lockDurationMs: 1000 })
      assert(!lock2, 'Lock was acquired twice')
      await lock?.release()
    }

    {
      // TEST 3: Acquire a lock, then try to acquire it again with polling and check it succeeds
      const lock = await Locks.acquire('testLock', { lockDurationMs: 1000 })
      const lock2 = await Locks.pollUntilAcquired('testLock', { lockDurationMs: 1000 })
      assert(lock2, 'Lock was not acquired after polling, but it should have succeeded')
      await lock?.release()
      await lock2?.release()
    }
  }
  void testLock().catch(err => {
    console.error(err)
    process.exit(1)
  })
}

// #endregion

// #region Utils: you can skip this region of code, it's only included so that the snippet is self-contained

function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

function isUndefined(x: unknown): x is undefined {
  return typeof x === 'undefined'
}

/**
 * If neither timeout nor attempts is provided, defaults to 30
 * attempts.
 * If only timeout is provided, attempts will be infinite.
 * If only attempts is provided, timeout will be infinite.
 * If both are provided, both will be used to limit the poll.
 */
const pollFor = async <T>(
  fn: () => Promise<T | undefined> | T | undefined,
  opts?: {
    /** Defaults to 0 - in milliseconds */
    interval?: number
    /** In milliseconds  */
    timeout?: number
    attempts?: number
  }
) => {
  let { interval = 0, timeout, attempts } = opts || {} // eslint-disable-line
  if (!isUndefined(timeout) && isUndefined(attempts)) attempts = Infinity
  attempts = isUndefined(attempts) ? 30 : attempts
  timeout = isUndefined(timeout) ? Infinity : timeout

  const start = Date.now()
  for (let i = 1; i < attempts + 1; i++) {
    const result = await fn()
    if (result !== undefined) return result
    if (i > attempts) return
    if (Date.now() - start > timeout) return
    await sleep(interval)
  }

  return
}

function assert(
  predicate: any,
  message: string,
  extra?: Record<string, unknown>
): asserts predicate {
  if (!predicate) {
    throw new AssertionError(message, extra)
  }
}

class AssertionError extends Error {
  extra: Record<string, unknown> | undefined
  constructor(message: string, extra: Record<string, unknown> | undefined = undefined) {
    super(message)
    this.name = 'AssertionError'
    this.extra = extra
  }
  toJSON() {
    return {
      message: this.message,
      name: this.name,
      extra: this.extra,
    }
  }
}

// #endregion

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Firebase Firestore iOS:如何将图像保存到 Firestore 中的集合中? - Firebase Firestore iOS: How can I save images to a collection in the firestore? 如何使用 Spring 引导 JPA/hibernate 在 Spanner 中使用 pessimistic_write 锁 - How to use pessimistic_write lock with Spanner using Spring boot JPA/hibernate 如何在 Firebase Firestore 上获取一个字段? - How can I get one field on Firebase Firestore? 如何判断我的值是否已存在于 Firebase Firestore 中? - How can I tell if I value already exists in Firebase Firestore? 如何将 Tasks 与 Firestore onSnapshotListener 结合使用? - How can I use Tasks with Firestore onSnapshotListener? Firestore - 如何在循环中使用 UpdateDoc? - Firestore - How Can I Use UpdateDoc in Loop? 在 firebase firestore 中,如何在不覆盖子集合的情况下更新文档? - in firebase firestore how can I update a document without overwriting subcollections? 如何按顺序显示 Firebase Firestore 数据? - How can I display Firebase Firestore data in order? 如何从 Firebase Firestore 获取 ReactJs 的数据? - How Can I get data with ReactJs from Firebase Firestore? 我如何判断它是图像还是视频 - Firebase Storage/Firestore - How can I tell if it's an image or video - Firebase Storage/Firestore
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM