显示提示后,LockService锁不会持续存在 [英] LockService lock does not persist after a prompt is shown

查看:99
本文介绍了显示提示后,LockService锁不会持续存在的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在Google表格中有一个脚本,当用户单击图像时该脚本会运行一个功能.该功能会修改单元格中的内容,为了避免同时进行修改,我需要为此功能使用锁.

I have a script in Google Sheets, which runs a function when a user clicks on an image. The function modifies content in cells and in order to avoid simultaneous modifications I need to use lock for this function.

我无法理解,为什么它不起作用(我仍然可以从不同的客户端多次调用相同的函数):

I cannot get, why this doesn't work (I still can invoke same function several times from different clients):

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

placeBid()函数如下:

placeBid() function is below:

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

我在Sheet上的不同元素上有几个placeBidMP()函数,并且只需要锁定单独的函数即可避免被多次调用.

I have several placeBidMP() functions for different elements on the Sheet and need to lock only separate function from being invoked multiple times.

我也尝试过以下方法:

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

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

and in this case, it shows cancellation pop-up straight away.

推荐答案

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

文档很清楚部分:prompt()方法不会持久化LockService锁定,因为它会挂起脚本执行以等待用户交互:

The documentation is clear on that part: prompt() method won't persist LockService locks as it suspends script execution awaiting user interaction:

在用户关闭对话框后,脚本将继续执行,但是Jdbc连接和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()方法签名-它返回void,这是一个伪造的值.您实际上是创建了以下代码:if(false),这就是为什么showCancelled()立即触发的原因.

Nothing strange here as well - if statement evaluates what's inside the condition and coerces the result to Boolean. Take a look at the waitLock() method signature - it returns void, which is a falsy value. You essentially created this: if(false) and this is why showCancelled() fires straight away.

解决方法

您可以通过模拟Lock类的作用来解决该限制.请注意,这种方法并不意味着要替换服务,并且也存在局限性,具体来说:

You could work around that limitation by emulating what Lock class does. Be aware that this approach is not meant to replace the service, and there are limitations too, specifically:

  1. PropertiesService在读取/写入时具有 quota .一个足够大的值,但您可能希望将toSleep时间间隔设置为较高的值,以避免以牺牲精度为代价来耗尽配额.
  2. 请勿使用此自定义实现替换Lock类-V8不会将您的代码放在特殊的上下文中,因此服务可以直接公开并且可以被覆盖.
  1. PropertiesService has quota on reads / writes. A generous one, but you might want to set toSleep interval to higher values to avoid burning through your quota at the expense of precision.
  2. Do not replace the Lock class with this custom implementation - V8 does not put your code in a special context, so the services are directly exposed and can be overridden.

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作为解决方法解决了一个重要问题:如果在执行获取锁的函数的执行过程中存在未处理的异常,则上述版本会使锁卡住无限期(可以通过删除相应的脚本属性来解决).

What follows below is closer to the original and solves one important issue with using PropertiesService as a workaround: if there is an unhandled exception during the execution of the function that acquires the lock, the version above will get the lock stuck indefinitely (can be solved by removing the corresponding script property).

以下版本(或作为要点)使用了基于时间的自我删除功能超过当前脚本的最大执行时间(30分钟)后,将触发设置为触发,并且可以将其配置为一个较小的值,以防有人希望提早清理:

The version below (or as a gist) uses a self-removing time-based trigger set to fire after the current maximum execution time of a script is exceeded (30 minutes) and can be configured to a lower value should one wish to clean up earlier:

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方法来强制执行此操作).

Note that the second version uses static methods and, just as LockService, should not be instantiated (you could go for a class and static methods to enforce this).

参考

  1. waitLock()方法引用
  2. prompt()方法参考
  3. 虚假概念在JavaScript中
  1. waitLock() method reference
  2. prompt() method reference
  3. Falsiness concept in JavaScript

这篇关于显示提示后,LockService锁不会持续存在的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆