Блокировка LockService не сохраняется после отображения запроса

У меня есть скрипт в Google Sheets, который запускает функцию, когда пользователь нажимает на изображение. Функция изменяет содержимое в ячейках, и, чтобы избежать одновременных изменений, мне нужно использовать блокировку для этой функции.

Я не могу понять, почему это не работает (я все еще могу вызывать одну и ту же функцию несколько раз с разных клиентов):

function placeBidMP1() {
  var lock = LockService.getScriptLock();
  lock.waitLock(10000)
  placeBid('MP1', 'I21:J25');
  lock.releaseLock();
}

Функция placeBid() находится ниже:

    function placeBid(lotName, range) {
      var firstPrompt = ui.prompt(lotName + '-lot', 'Please enter your name:', ui.ButtonSet.OK); 
      var firstPromptSelection = firstPrompt.getSelectedButton(); 
      var userName = firstPrompt.getResponseText();
  
    if (firstPromptSelection == ui.Button.OK) {
    
      do {
        
        var secondPrompt = ui.prompt('Increase by', 'Amount (greater than 0): ', ui.ButtonSet.OK_CANCEL);  
        var secondPromptSelection = secondPrompt.getSelectedButton(); 
        var increaseAmount = parseInt(secondPrompt.getResponseText());
        
      } while (!(secondPromptSelection == ui.Button.CANCEL) && !(/^[0-9]+$/.test(increaseAmount)) && !(secondPromptSelection == ui.Button.CLOSE));
    
    if (secondPromptSelection != ui.Button.CANCEL & secondPromptSelection != ui.Button.CLOSE) {
      
        var finalPrompt = ui.alert("Price for lot will be increased by " + increaseAmount + " CZK. Are you sure?", ui.ButtonSet.YES_NO);
        if (finalPrompt == ui.Button.YES) {
          
          var cell = SpreadsheetApp.getActiveSheet().getRange(range);
          var currentCellValue = Number(cell.getValue());
          cell.setValue(currentCellValue + Number(increaseAmount));
          bidsHistorySheet.appendRow([userName, lotName, cell.getValue()]);
          SpreadsheetApp.flush();
          showPriceIsIncreased();
          
        } else {showCancelled();}
    } else {showCancelled();}
  } else {showCancelled();}
}

У меня есть несколько placeBidMP() функции для разных элементов на листе и нужно заблокировать только отдельные функции от многократного вызова.

Я тоже пробовал следующий способ:

if (lock.waitLock(10000)) {
 placeBidMP1(...);
} 
else {
 showCancelled();
}

и в этом случае он сразу показывает всплывающее окно отмены.

1 ответ

Решение

Я все еще могу вызывать одну и ту же функцию несколько раз с разных клиентов

Документация ясно на этой части:prompt() метод не сохраняется LockService блокируется, поскольку приостанавливает выполнение скрипта в ожидании взаимодействия с пользователем:

Сценарий возобновляется после того, как пользователь закрывает диалоговое окно, но соединения Jdbc и блокировки LockService не сохраняются во время приостановки.

и в этом случае он сразу показывает всплывающее окно отмены

Здесь тоже ничего странного - ifЗаявление оценивает то, что внутри условия и принуждает результат вBoolean. Взгляните наwaitLock() подпись метода - возвращает void, что является ложным значением. По сути, вы создали это:if(false) и вот почему showCancelled() срабатывает сразу.

Обходной путь

Вы можете обойти это ограничение, эмулируя то, что Lockкласс делает. Имейте в виду, что этот подход не предназначен для замены службы, и есть ограничения, в частности:

  1. PropertiesServiceимеет квоту на чтение / запись. Щедрый, но вы можете установитьtoSleep интервал до более высоких значений, чтобы избежать перерасхода квоты за счет точности.
  2. Не заменяйте Lock класс с этой настраиваемой реализацией - V8 не помещает ваш код в особый контекст, поэтому службы доступны напрямую и могут быть переопределены.
function PropertyLock() {

  const toSleep = 10;

  let timeoutIn = 0, gotLock = false;

  const store = PropertiesService.getScriptProperties();

  /**
   * @returns {boolean}
   */
  this.hasLock = function () {
    return gotLock;
  };

  /**
   * @param {number} timeoutInMillis 
   * @returns {boolean}
   */
  this.tryLock = function (timeoutInMillis) {

    //emulates "no effect if the lock has already been acquired"
    if (this.gotLock) {
      return true;
    }

    timeoutIn === 0 && (timeoutIn = timeoutInMillis);

    const stored = store.getProperty("locked");
    const isLocked = stored ? JSON.parse(stored) : false;

    const canWait = timeoutIn > 0;

    if (isLocked && canWait) {
      Utilities.sleep(toSleep);

      timeoutIn -= toSleep;

      return timeoutIn > 0 ?
        this.tryLock(timeoutInMillis) :
        false;
    }

    if (!canWait) {
      return false;
    }

    store.setProperty("locked", true);

    gotLock = true;

    return true;
  };

  /**
   * @returns {void}
   */
  this.releaseLock = function () {

    store.setProperty("locked", false);

    gotLock = false;
  };

  /**
   * @param {number} timeoutInMillis
   * @returns {boolean}
   * 
   * @throws {Error}
   */
  this.waitLock = function (timeoutInMillis) {
    const hasLock = this.tryLock(timeoutInMillis);

    if (!hasLock) {
      throw new Error("Could not obtain lock");
    }

    return hasLock;
  };
}

Версия 2

Следующее ниже ближе к оригиналу и решает одну важную проблему с использованием PropertiesService в качестве обходного пути: если есть необработанное исключение во время выполнения функции, которая получает блокировку, в приведенной выше версии блокировка застревает на неопределенное время (можно решить, удалив соответствующее свойство скрипта).

В приведенной ниже версии (или в сущности) используется самоуничтожающийся триггер на основе времени, который срабатывает после превышения текущего максимального времени выполнения скрипта (30 минут), и его можно настроить на более низкое значение, если кто-то захочет очистить ранее:

var PropertyLock = (() => {

    let locked = false;
    let timeout = 0;

    const store = PropertiesService.getScriptProperties();

    const propertyName = "locked";
    const triggerName = "PropertyLock.releaseLock";

    const toSleep = 10;
    const currentGSuiteRuntimeLimit = 30 * 60 * 1e3;

    const lock = function () { };

    /**
     * @returns {boolean}
     */
    lock.hasLock = function () {
        return locked;
    };

    /**
     * @param {number} timeoutInMillis 
     * @returns {boolean}
     */
    lock.tryLock = function (timeoutInMillis) {

        //emulates "no effect if the lock has already been acquired"
        if (locked) {
            return true;
        }

        timeout === 0 && (timeout = timeoutInMillis);

        const stored = store.getProperty(propertyName);
        const isLocked = stored ? JSON.parse(stored) : false;

        const canWait = timeout > 0;

        if (isLocked && canWait) {
            Utilities.sleep(toSleep);

            timeout -= toSleep;

            return timeout > 0 ?
                PropertyLock.tryLock(timeoutInMillis) :
                false;
        }

        if (!canWait) {
            return false;
        }

        try {
            store.setProperty(propertyName, true);

            ScriptApp.newTrigger(triggerName).timeBased()
                .after(currentGSuiteRuntimeLimit).create();

            console.log("created trigger");
            locked = true;

            return locked;
        }
        catch (error) {
            console.error(error);
            return false;
        }
    };

    /**
     * @returns {void}
     */
    lock.releaseLock = function () {

        try {
            locked = false;
            store.setProperty(propertyName, locked);

            const trigger = ScriptApp
                .getProjectTriggers()
                .find(n => n.getHandlerFunction() === triggerName);

                console.log({ trigger });

            trigger && ScriptApp.deleteTrigger(trigger);
        }
        catch (error) {
            console.error(error);
        }

    };

    /**
     * @param {number} timeoutInMillis
     * @returns {boolean}
     * 
     * @throws {Error}
     */
    lock.waitLock = function (timeoutInMillis) {
        const hasLock = PropertyLock.tryLock(timeoutInMillis);

        if (!hasLock) {
            throw new Error("Could not obtain lock");
        }

        return hasLock;
    };

    return lock;
})();

var PropertyLockService = (() => {
    const init = function () { };

    /**
     * @returns {PropertyLock}
     */
    init.getScriptLock = function () {
        return PropertyLock;
    };

    return init;
})();

Обратите внимание, что вторая версия использует статические методы и, как и LockService, не следует создавать экземпляры (вы можете использовать class а также static методы для обеспечения этого).

Ссылки

  1. waitLock() ссылка на метод
  2. prompt() ссылка на метод
  3. Концепция ложности в JavaScript
Другие вопросы по тегам