Naar inhoud
lightbulb Welkom op de nieuwe kennisbank | We hebben de docs volledig vernieuwd met meer dan 160 features. Bekijk wat nieuw isarrow_forward

Concurrente toegang in Apps Script

Beheer gelijktijdige scripttoegang veilig met de Lock Service, idempotente operaties en optimistische vergrendeling in Apps Script.

Concurrentieproblemen in Apps Script

Wanneer meerdere script-instanties tegelijk dezelfde data bewerken, ontstaan race conditions. Typische scenario's:

  1. Meerdere formulierinzendingen tegelijk.
  2. Een trigger-run die begint terwijl de vorige nog loopt.
  3. Twee gebruikers die dezelfde spreadsheet-data bijwerken.
  4. Een webhook die meerdere keren razendsnel wordt aangeroepen.
function onFormSubmit_ONVEILIG(e) {
  const blad = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Log');
  const volgNummer = blad.getLastRow();
  blad.appendRow([volgNummer + 1, e.response.getRespondentEmail()]);
}

Twee gelijktijdige inzendingen lezen beide getLastRow() voordat er geschreven is, en genereren hetzelfde volgnummer. Het resultaat is een dubbel nummer of een overschreven rij.

info

Apps Script voert elke uitvoering in een aparte instantie uit. Er is geen gedeeld geheugen tussen die instanties, dus je kunt niet vertrouwen op een variabele om de staat te bewaken. Gebruik daarvoor de Lock Service of PropertiesService.

Veilige formulierverwerking met locks

De LockService geeft je drie soorten locks: getScriptLock() blokkeert alle gebruikers, getDocumentLock() blokkeert binnen één document en getUserLock() blokkeert per gebruiker. Voor gedeelde resources zoals een logblad gebruik je vrijwel altijd de script-lock.

function onFormSubmit(e) {
  const lock = LockService.getScriptLock();

  try {
    lock.waitLock(30000);

    const blad = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Registraties');
    const volgNummer = blad.getLastRow();
    const tijdstip = new Date();
    const email = e.response.getRespondentEmail();

    blad.appendRow([volgNummer, tijdstip, email, 'Nieuw']);
    SpreadsheetApp.flush();

  } finally {
    lock.releaseLock();
  }
}

waitLock(ms) wacht maximaal het opgegeven aantal milliseconden en gooit een fout als de lock niet vrijkomt. tryLock(ms) doet hetzelfde maar geeft false terug in plaats van een fout. Geef de lock altijd vrij in een finally-blok.

lightbulb

Vergeet SpreadsheetApp.flush() niet

Apps Script bundelt schrijfacties naar Sheets en voert ze pas door aan het einde van de uitvoering. Roep SpreadsheetApp.flush() aan voordat je de lock vrijgeeft, anders kan een wachtende instantie nog de oude data lezen terwijl jouw schrijfactie nog in de buffer staat.

Idempotente operaties

Een idempotente operatie geeft hetzelfde resultaat, ongeacht hoe vaak je hem uitvoert. Dit is de veiligste aanpak voor situaties waar je niet zeker bent of een operatie al is uitgevoerd, zoals bij webhooks die opnieuw worden geprobeerd.

function idempotentGebruikerAanmaken(email, naam) {
  const lock = LockService.getScriptLock();

  try {
    lock.waitLock(10000);

    const blad = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Gebruikers');
    const data = blad.getDataRange().getValues();

    const bestaatAl = data.some(rij => rij[0] === email);
    if (bestaatAl) {
      Logger.log(`Gebruiker ${email} bestaat al, overgeslagen`);
      return false;
    }

    blad.appendRow([email, naam, new Date(), 'Actief']);
    Logger.log(`Gebruiker ${email} aangemaakt`);
    return true;

  } finally {
    lock.releaseLock();
  }
}
info

Ontwerp operaties als idempotent wanneer ze kunnen worden aangeroepen door webhooks of opnieuw afgevuurde triggers. Een check op bestaande data voordat je schrijft is eenvoudig en effectief.

Optimistische vergrendeling met versienummers

Bij optimistische vergrendeling ga je ervan uit dat conflicten zeldzaam zijn. Elke record krijgt een versienummer; bij het bijwerken controleer je of de versie nog klopt. Komt die niet overeen, dan heeft iemand anders de record intussen aangepast en weiger je de schrijfactie.

function bijwerkenMetVersiecontrole(recordId, nieuweWaarde, verwachteVersie) {
  const lock = LockService.getScriptLock();

  try {
    lock.waitLock(10000);

    const blad = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Records');
    const data = blad.getDataRange().getValues();

    const rijIdx = data.findIndex(r => r[0] === recordId);
    if (rijIdx === -1) throw new Error(`Record ${recordId} niet gevonden`);

    const huidigVersie = data[rijIdx][3];
    if (huidigVersie !== verwachteVersie) {
      throw new Error(`Versieconflict: verwacht ${verwachteVersie}, actueel ${huidigVersie}`);
    }

    blad.getRange(rijIdx + 1, 2).setValue(nieuweWaarde);
    blad.getRange(rijIdx + 1, 4).setValue(verwachteVersie + 1);
    Logger.log(`Record ${recordId} bijgewerkt naar versie ${verwachteVersie + 1}`);

  } finally {
    lock.releaseLock();
  }
}

Dubbele trigger-uitvoeringen voorkomen

Met tryLock(0) probeer je een lock zonder te wachten. Lukt dat niet, dan loopt er al een andere instantie en sla je deze run gewoon over.

function dagelijkseImport() {
  const lock = LockService.getScriptLock();

  if (!lock.tryLock(0)) {
    Logger.log('dagelijkseImport wordt al uitgevoerd, deze run overgeslagen');
    return;
  }

  try {
    Logger.log('Import gestart');
    Utilities.sleep(3000);
    Logger.log('Import klaar');
  } finally {
    lock.releaseLock();
  }
}

Wachtrij-patroon voor seriële verwerking

Soms wil je taken niet weigeren maar netjes achter elkaar afhandelen. Bewaar dan een wachtrij in PropertiesService en verwerk telkens één taak per run.

function voegToeAanWachtrij(taak) {
  const lock = LockService.getScriptLock();

  try {
    lock.waitLock(10000);

    const props = PropertiesService.getScriptProperties();
    const wachtrij = JSON.parse(props.getProperty('WACHTRIJ') || '[]');
    wachtrij.push({...taak, id: Utilities.getUuid(), tijdstip: new Date().toISOString()});
    props.setProperty('WACHTRIJ', JSON.stringify(wachtrij));

  } finally {
    lock.releaseLock();
  }
}

function verwerkWachtrij() {
  const lock = LockService.getScriptLock();

  if (!lock.tryLock(1000)) {
    Logger.log('Wachtrij wordt al verwerkt');
    return;
  }

  try {
    const props = PropertiesService.getScriptProperties();
    const wachtrij = JSON.parse(props.getProperty('WACHTRIJ') || '[]');

    if (wachtrij.length === 0) return;

    const taak = wachtrij.shift();
    props.setProperty('WACHTRIJ', JSON.stringify(wachtrij));

    lock.releaseLock();

    verwerkTaak(taak);
  } catch(e) {
    lock.releaseLock();
    throw e;
  }
}

function verwerkTaak(taak) {
  Logger.log(`Verwerken: ${JSON.stringify(taak)}`);
}
warning

Let op de uitvoeringslimiet

Elke Apps Script-uitvoering stopt na 6 minuten, ook bij Google Workspace-accounts. Een lock wordt automatisch vrijgegeven zodra het script eindigt, dus een lock kan nooit langer dan die 6 minuten blijven hangen. Houd het werk binnen een gelockt blok kort, zodat wachtende instanties niet onnodig in waitLock blijven hangen.

Thread-veilige teller implementeren

  1. Vraag een lock aan met LockService.getScriptLock().
  2. Wacht op exclusieve toegang met lock.waitLock(10000).
  3. Lees de huidige waarde uit PropertiesService.
  4. Verhoog de waarde in JavaScript, zonder extra service-aanroep.
  5. Schrijf de nieuwe waarde terug naar PropertiesService.
  6. Geef de lock vrij in een finally-blok.
Hoe weet ik of twee triggers tegelijk lopen?

Gebruik LockService.getScriptLock().tryLock(0). Als dit false retourneert, loopt er al een andere instantie en kun je deze run veilig overslaan.

Wat als een lock vast blijft zitten?

Dat kan in de praktijk niet lang gebeuren. Een lock wordt automatisch vrijgegeven zodra het script eindigt, en elke uitvoering stopt na maximaal 6 minuten. Wil je toch zelf vastgelopen werk detecteren, bewaar dan een starttijdstempel in PropertiesService en negeer een claim die ouder is dan jouw drempel.

Kunnen meerdere gebruikers gelijktijdig in een Sheets-gebonden script werken?

Ja. Elke gebruiker die het script triggert, bijvoorbeeld via een menu-item, start een aparte uitvoering. Gebruik een script-lock om gedeelde resources te beschermen.

Is er een maximum wachttijd voor waitLock?

Ja. waitLock(ms) wacht maximaal het opgegeven aantal milliseconden en gooit dan een fout. Houd de wachttijd kort, vaak 10 tot 30 seconden, om gebruikersonvriendelijke vertragingen te voorkomen.

Wat is het verschil tussen waitLock en tryLock?

Beide proberen een lock binnen een opgegeven tijd te krijgen. waitLock gooit een fout als dat niet lukt, tryLock geeft simpelweg false terug. Gebruik tryLock als je netjes wilt afhandelen dat de lock bezet is, en waitLock als je echt moet wachten.

Welke lock kies ik: script, document of gebruiker?

Kies getScriptLock() voor resources die door iedereen worden gedeeld, getDocumentLock() als de bescherming alleen binnen één document hoeft, en getUserLock() om te voorkomen dat dezelfde gebruiker een functie dubbel afvuurt.

Concurrente toegang is een onderschat probleem in Apps Script. Door consistent gebruik van locks en idempotente operaties bouw je scripts die ook onder belasting betrouwbaar functioneren.