显示提示后,LockService 锁不会持续存在

LockService lock does not persist after a prompt is shown

我在 Google Sheet 中有一个脚本,它在用户单击图像时运行一个函数。该函数修改单元格中的内容,为了避免同时修改,我需要为此函数使用锁定。

我不明白为什么这不起作用(我仍然可以从不同的客户端多次调用相同的函数):

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() 函数用于 Sheet 上的不同元素,并且只需要锁定单独的函数以免被多次调用。

我也试过下一个方法:

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

在这种情况下,它会立即显示取消弹出窗口。

我仍然可以从不同的客户端多次调用同一个函数

documentation 在那部分很清楚:prompt() 方法不会持续 LockService 锁定,因为它暂停脚本执行等待用户交互:

The script resumes after the user dismisses the dialog, but Jdbc connections and LockService locks don't persist across the suspension

在这种情况下,它会立即显示取消弹出窗口

这里也没什么奇怪的 - if 语句评估条件内的内容, 将结果强制 Boolean。查看 waitLock() 方法签名 - 它 returns void,这是一个虚假值。您基本上创建了这个:if(false),这就是 showCancelled() 立即触发的原因。

解决方法

您可以通过模仿 Lock class 的作用来解决该限制。请注意,这种方法并不是要取代服务,而且也有局限性,具体来说:

  1. PropertiesService 具有 quota 读/写。一个慷慨的,但您可能希望将 toSleep 间隔设置为更高的值,以避免以牺牲精度为代价来消耗您的配额。
  2. 不要用这个自定义实现替换 Lock class - 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 作为变通方法的一个重要问题:如果在执行获取锁的函数期间出现未处理的异常,则上面的版本将无限期地锁住(可以通过删除相应的脚本来解决属性)。

以下版本(或as a gist)使用一个自删除的基于时间的触发器设置为在超过当前脚本的最大执行时间(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 一样,不应实例化(您可以使用 classstatic 方法来强制执行此操作)。

参考资料

  1. waitLock()方法reference
  2. prompt()方法reference
  3. JavaScript
  4. 中的错误 concept