Mutex Implementation - HeroPhil/DHBW-KinoCinema GitHub Wiki

Allgemeines

Zur Umsetzung der Buchung eines Tickets benötigen wir die Mutex Technologie (gegenseitiger Ausschluss zweier Ausführungen). Wir haben uns hierzu an diesem Beispiel orientiert.

Ablauf

Wenn zwei Ausführungen der Funktion zur Buchung desselben Tickets gleichzeitig gestartet werden muss sichergestellt werden, dass nur eine Ausführung in der Buchung des Tickets resultiert. Hierzu legen wir zunächst über eine Datenbank-Transaktion eine Berechtigung zur Buchung des Tickets ab. Nur wer diese Berechtigung besitzt, darf das eigentliche Ticket buchen. Die Datenbank Transaktion ist atomar, da beide Ausführungen versuchen die Berechtigung an derselben Stelle in der Datenbank abzulegen (/live/sync/screenings/${screening_id}/${row}/${seat}). Der gegenseitige Ausschluss findet also auf der untersten Ebene statt (dem eigentlichen Sitz). So können zwei verschiedene Sitze innerhalb einer Vorstellung immer noch parallel gebucht werden. Wenn zwei Nutzer gleichzeitig versuchen die Berechtigung zur Buchung des Tickets in der Datenbank abzulegen, wird dies nur einem der beiden gelingen. Der Zugriff auf ein Dokument in der Datenbank ist gekapselt. So kann nur ein Nutzer zur Zeit die Berechtigung anlegen. Der andere Nutzer sieht, dass bereits eine (fremde) Berechtigung existiert und wird seine Datenbank Transaktion abbrechen. In der Berechtigung ist eine AusführungsID hinterlegt, die sonst nur innerhalb der eigenen Ausführung bekannt ist. So kann jede Ausführung feststellen, ob die Berechtigung in der Datenbank für diese oder eine fremde / andere Ausführung bestimmt ist.

Beispiel Schema

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const {nanoid} = require('nanoid');

admin.initializeApp();

exports.withLock = functions.pubsub
  .schedule("0 * * * *")
  .onRun((context) => {
    const { eventId, timestamp } = context;

    const executionId = nanoid();
    const lockRef = admin.firestore().collection("locks").doc(eventId);

    const locked = await admin.firestore().runTransaction(async (transaction) => {
      const lockSnapshot = await transaction.get(lockRef);

      if (lockSnapshot.exists) {
        // If the lock exists, check if we locked it
        return lockSnapshot.data().executionId === executionId;
      }
  
      // If it doesn't exist, attempt to create it
      await transaction.set(lockRef, {
        executionId,
        // The timestamp and lockedAt date are just for debugging, why not?
        timestamp,
        lockedAt: new Date(),
      });
  
      // If the write succeeds, we own the lock
      return true;
    });
  
    if (locked !== true) {
      throw new Error("Already locked. This pubsub event has already been serviced.");
    }
  
    console.log("Locked");

    // Do important stuff
    });