LockService lock does not persist after a prompt is shown

送分小仙女□ 提交于 2020-08-20 05:58:16

问题


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

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

I've tried as well next way:

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

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


回答1:


I still can invoke the same function several times from different clients

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

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

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

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.

Workaround

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

Version 2

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).

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

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).

References

  1. waitLock() method reference
  2. prompt() method reference
  3. Falsiness concept in JavaScript


来源:https://stackoverflow.com/questions/62195189/lockservice-lock-does-not-persist-after-a-prompt-is-shown

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!