【问题标题】:LockService lock does not persist after a prompt is shown显示提示后,LockService 锁不会持续存在
【发布时间】:2020-08-07 21:09:31
【问题描述】:

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

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

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();
}

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

【问题讨论】:

    标签: google-apps-script google-sheets locking


    【解决方案1】:

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

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

    脚本在用户关闭对话框后恢复,但 Jdbc 连接和 LockService 锁不会在暂停期间持续存在

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

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

    解决方法

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

    1. PropertiesService 在读/写上有 quota。一个慷慨的,但您可能希望将 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作为解决方法的一个重要问题:如果在获取锁的函数执行期间出现未处理的异常,则上面的版本会卡住锁无限期(可以通过删除相应的脚本属性来解决)。

    以下版本(或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 中的虚假concept

    【讨论】:

    • 感谢您的回答!在此处发布之前正在阅读此文档并且没有找到与提示暂停效果相关的任何内容:(在这种情况下,是否有任何其他潜在的解决方案可以解决我的问题?我只需要确保 2 个人不能修改 1 个特定单元格在运行脚本的同时。
    • 不用担心——我们都会时不时感到眼花缭乱 :) PropertiesService 有一个解决方法,当我确定它可以正常工作时,我会在这里发布。您还可以做的是将您的逻辑移到提示之外并仅在实际编辑发生时锁定 - 这会导致更糟糕的用户体验,但可能是唯一的其他选择
    猜你喜欢
    • 1970-01-01
    • 2020-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-05
    • 2017-02-16
    相关资源
    最近更新 更多