Блокировка 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
класс делает. Имейте в виду, что этот подход не предназначен для замены службы, и есть ограничения, в частности:
PropertiesService
имеет квоту на чтение / запись. Щедрый, но вы можете установитьtoSleep
интервал до более высоких значений, чтобы избежать перерасхода квоты за счет точности.- Не заменяйте
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
методы для обеспечения этого).
Ссылки