脚本运行时限制执行时间

45

我的 Google 应用脚本正在遍历用户的 Google Drive 文件,并将文件复制或移动到其他文件夹中。但是在运行一定时间后,脚本总是停止,日志中没有任何错误信息。


编辑注:限制时间因账户类型(免费或付费)和时间而异。截至 2022 年 12 月,大部分答案仍然有效。


我会在一次运行中处理几十个甚至上千个文件。

是否有任何设置或解决方法?


2
您可以使用HTML Service在工作子集上启动脚本的单独“迭代”,从而打破规则。 Bruce McPherson已经发表了相关博客 - Mogsdad
1
如果您是商业客户,现在可以注册App Maker的早期访问,其中包括灵活的配额 - browly
相关链接:https://stackoverflow.com/q/63604878/ - TheMaster
截至2022年8月,对于付费Workspace帐户,我看到执行时间限制为1800秒=半小时。 - Doochz
12个回答

97

你可以尝试做一件事情(当然这取决于你想要实现什么),方法如下:

  1. 将必要的信息(例如循环计数器)存储在电子表格或其他永久存储位置(如ScriptProperties)中。
  2. 每隔五分钟左右终止脚本运行。
  3. 设置定时触发器以每隔五分钟运行该脚本(或使用Script服务编程创建触发器)。
  4. 每次运行从你所用的永久存储中读取保存的数据,并从上次停止的地方继续执行脚本。

这不是万能解决方案,如果你发布代码,人们会更好地帮助你。

下面是我每天使用的脚本的简化代码摘录:

function runMe() {
  var startTime= (new Date()).getTime();
  
  //do some work here
  
  var scriptProperties = PropertiesService.getScriptProperties();
  var startRow= scriptProperties.getProperty('start_row');
  for(var ii = startRow; ii <= size; ii++) {
    var currTime = (new Date()).getTime();
    if(currTime - startTime >= MAX_RUNNING_TIME) {
      scriptProperties.setProperty("start_row", ii);
      ScriptApp.newTrigger("runMe")
               .timeBased()
               .at(new Date(currTime+REASONABLE_TIME_TO_WAIT))
               .create();
      break;
    } else {
      doSomeWork();
    }
  }
  
  //do some more work here
  
}

注意1:变量REASONABLE_TIME_TO_WAIT应足够长,以便新触发器能够触发。(我将其设置为5分钟,但我认为可以少于这个时间)。

注意2:doSomeWork()必须是一个相对较快执行的函数(我会说少于1分钟)。

注意3:Google已经弃用了Script Properties,并引入了Properties Service。该函数已相应地进行了修改。

注意4:第二次调用函数时,它将使用循环的第i个值作为字符串,因此您必须将其转换为整数。


1
触发器的执行次数有限制吗?我认为可能每24小时会有一个触发器限制之类的...谢谢! - Kalin
1
我认为这对于插件不起作用。插件定时触发器只允许每小时执行一次。你知道其他保持任务运行并处理大量来自Excel表的数据的解决方案吗? - angelokh
1
Google已经弃用了这种方法。有没有其他的替代方案?https://developers.google.com/apps-script/reference/properties/script-properties - iamtoc
1
@iamtoc 虽然脚本属性被禁用,但您仍然可以使用PropertiesService。这只需要进行非常小的编辑。 - JZL003
1
REASONABLE_TIME_TO_WAIT 的用途是什么,我们不能只使用 .at(new Date(currTime)) 吗? - Valip
显示剩余4条评论

71

限制

单个脚本的最大执行时间为6分钟/次。
- https://developers.google.com/apps-script/guides/services/quotas

除此之外,还有其他需要了解的限制。例如,您每天只允许总触发器运行时间为1小时,因此不能将长函数分成12个不同的5分钟块。

优化

话虽如此,很少情况下你真正需要用六分钟来执行。JavaScript在几秒钟内就可以轻松排序数千行数据。造成性能问题的可能是对Google应用程序自身的服务调用。

您可以编写脚本以最大程度地利用内置缓存,通过减少读取和写入的次数来实现。交替使用读取和写入命令会变慢。要加速脚本,请使用一个命令将所有数据读入数组中,在数组中执行任何数据操作,然后使用一个命令将数据写出。
- https://developers.google.com/apps-script/best_practices

批处理

最好的方法是减少服务调用的数量。谷歌通过允许大部分API调用的批量版本来实现这一点。

例如,不要使用以下代码:

for (var i = 1; i <= 100; i++) {
  SpreadsheetApp.getActiveSheet().deleteRow(i);
}

做这个:

SpreadsheetApp.getActiveSheet().deleteRows(i, 100);

在第一个循环中,您不仅需要对表格执行100次deleteRow调用,还需要100次获取活动表。第二种变化应该比第一种性能好数个数量级。

交织读写操作

此外,您还应非常注意不要频繁地在读写之间来回切换。这样不仅会失去批量操作的潜在收益,而且Google也无法使用其内置缓存。

每次读取时,我们必须首先清空(提交)写缓存,以确保您读取到最新数据(您可以通过调用SpreadsheetApp.flush()强制将缓存写入)。同样,每次写入时,我们必须丢弃读缓存,因为它不再有效。因此,如果您可以避免交错读写,您将获得缓存的全部好处。
- http://googleappsscript.blogspot.com/2010/06/optimizing-spreadsheet-operations.html

例如,不要这样做:

sheet.getRange("A1").setValue(1);
sheet.getRange("B1").setValue(2);
sheet.getRange("C1").setValue(3);
sheet.getRange("D1").setValue(4);

请执行以下操作

sheet.getRange("A1:D1").setValues([[1,2,3,4]]);

链式函数调用

作为最后的手段,如果你的函数真的无法在六分钟内完成,你可以将调用链接起来或将函数拆分成适用于较小数据段的形式。

你可以将数据存储在 缓存服务(临时)或 属性服务(永久)的存储桶中进行跨执行检索(因为 Google 应用脚本具有无状态执行)。

如果你想启动另一个事件,你可以使用Trigger Builder Class 创建自己的触发器,或在紧密的时间表上设置定期触发器。


1
谢谢KyleMit,这是一个非常全面的答案! - Laurent'
7
尽管如此,实际上很少有理由需要花费6分钟的时间来执行。请尝试编写一个可以处理Gmail、Drive等内容的脚本... - user541686
@Mehrdad,这些似乎是一些原因:) 但是没错,95%+的脚本不应该遇到这种路障。 - KyleMit

34

另外,尽量减少对谷歌服务的调用次数。例如,如果你想更改电子表格中一系列单元格的值,请不要读取每个单元格、更改它并将其存回。 相反,一次性将整个范围(使用Range.getValues())读入内存、进行更改并全部存储(使用Range.setValues())。

这样可以节省大量执行时间。


17

Anton Soradoi的答案看起来还可以,但考虑使用高速缓存服务而不是将数据存储到临时表中。

 function getRssFeed() {
   var cache = CacheService.getPublicCache();
   var cached = cache.get("rss-feed-contents");
   if (cached != null) {
     return cached;
   }
   var result = UrlFetchApp.fetch("http://example.com/my-slow-rss-feed.xml"); // takes 20 seconds
   var contents = result.getContentText();
   cache.put("rss-feed-contents", contents, 1500); // cache for 25 minutes
   return contents;
 }

请注意,从2014年4月起,脚本运行时间的限制为6分钟。


G Suite Business / Enterprise / Education和Early Access用户:

从2018年8月起,这些用户的最大脚本运行时间现在设定为30分钟。


1
这对我来说似乎是解决问题最简单的方法,因为您不需要设置或关心任何其他资源(如电子表格、数据库等),而且所有脚本逻辑都保留在脚本本身中。谢谢! - dubrox
1
请问您能否举一个通用函数的例子? - Br. Sayan
1
这是一种非常优秀的方法,特别是可以避免频繁调用API并触发限制。 - tisaconundrum

12

找到一种方法来分割您的工作,以便它花费的时间少于6分钟,因为这是任何脚本的限制。在第一次遍历时,您可以迭代并将文件和文件夹列表存储在电子表格中,并添加一个基于时间的触发器用于第二部分。

在第二部分中,处理每个条目时删除列表中的每个条目。当列表中没有项目时,删除触发器。

这就是我如何处理大约1500行的表格,将其分散到大约十几个不同的电子表格中。由于对电子表格的调用次数太多,它会超时,但在触发器再次运行时会继续进行。


1
准确来说,最大执行时间为6分钟:“当前脚本执行时间限制的最大值(6分钟)”,如此处所述:https://developers.google.com/apps-script/scriptdb。 - Peter
谢谢,我已经修复了。 此外,我在我的脚本中使用了10分钟的触发器,只是为了确保执行之间没有重叠。我不确定Google如何决定启动时间驱动的触发器,所以留有一点缓冲也无妨。 - Fred
所以你可以将所有数据存储到ScriptDb中,只需完成一小部分(因为6分钟限制),然后在下一次运行中继续(由计时器触发)。这听起来是一个不错的解决方案。 - Martin V.
此外,现在您可以创建任何时间的触发器,因此我的脚本每次启动时都会创建一个7分钟后的触发器(如果它知道必须继续运行)。 - Fred

6

我在循环处理大量信息时使用了ScriptDB来保存我的位置。脚本可能会超过5分钟的限制。通过在每次运行时更新ScriptDb,脚本可以从数据库中读取状态并继续上次离开的地方,直到所有处理完成。尝试使用这种策略,我相信你会对结果感到满意。


1
在处理一个遍历电子表格上 750 封电子邮件地址的脚本时,我遇到了类似的问题。如何存储脚本停止的位置并恢复执行? - jwesonga
2
ScriptDb已被弃用。 - Rubén

6
如果您正在使用G Suite Business或Enterprise版本,您可以在App Maker启用您的脚本运行时之后提前注册App Maker,此时运行时间将从6分钟增加到30分钟 :)。
有关App Maker的更多详细信息,请点击这里

是的,我们可以使用早期访问计划将运行时间从6分钟增加到30分钟,但这些应用程序不能部署到公共环境中。 - Sharath
1
App Maker 产品将于2021年1月19日停止运营。 https://support.google.com/a/answer/9682494?p=am_announcement - King Holly
除了App Maker即将关闭之外,没有必要注册早期访问以获得30分钟的限制。 - Rubén
1
这个答案已经过时了。App Maker已经关闭,Apps Script配额页面已更新。截至2023年6月15日,消费者和Workspace账户的脚本运行时间限制为6分钟。有关当前配额的参考资料,请访问:https://developers.google.com/apps-script/guides/services/quotas - Rubén

1

这是一种基于Dmitry Kostyuk的绝佳文章的方法,与其不同之处在于它不尝试定时执行并优雅地退出。相反,它每分钟故意生成一个新线程,并让它们运行,直到被Google超时。这避免了最大执行时间限制,并通过在多个线程中并行运行处理来加速处理速度。(即使您没有达到执行时间限制,这也会加速处理速度。)

它在脚本属性中跟踪任务状态,以及一个信号量以确保任何时候都没有两个线程正在编辑任务状态。(它使用多个属性,因为每个属性限制为9k。)

我试图模仿Google Apps Script的iterator.next() API,但不能使用iterator.hasNext(),因为那样不是线程安全的(参见TOCTOU)。它在底层使用了几个外观类。

如果有任何建议,我将非常感激。这对我很有效,通过生成三个并行线程来运行文档目录中的文件,将处理时间减少了一半。您可以在配额内生成20个,但这已经足够满足我的用例。

该类旨在成为插入即用,可用于任何目的而无需修改。唯一需要用户执行的操作是在处理文件时删除任何来自先前超时尝试的输出。如果Google在处理任务完成之前超时,则迭代器会多次返回给定的fileId

为了消除日志记录,所有日志都通过底部的log()函数进行。

以下是使用方法:

const main = () => {
  const srcFolder = DriveApp.getFoldersByName('source folder',).next()
  const processingMessage = processDocuments(srcFolder, 'spawnConverter')
  log('main() finished with message', processingMessage)
}

const spawnConverter = e => {
  const processingMessage = processDocuments()
  log('spawnConverter() finished with message', processingMessage)
}

const processDocuments = (folder = null, spawnFunction = null) => {
  // folder and spawnFunction are only passed the first time we trigger this function,
  // threads spawned by triggers pass nothing.
  // 10,000 is the maximum number of milliseconds a file can take to process.
  const pfi = new ParallelFileIterator(10000, MimeType.GOOGLE_DOCS, folder, spawnFunction)
  let fileId = pfi.nextId()
  const doneDocs = []
  while (fileId) {
    const fileRelativePath = pfi.getFileRelativePath(fileId)
    const doc = DocumentApp.openById(fileId)
    const mc = MarkupConverter(doc)

    // This is my time-consuming task:
    const mdContent = mc.asMarkdown(doc)

    pfi.completed(fileId)
    doneDocs.push([...fileRelativePath, doc.getName() + '.md'].join('/'))
    fileId = pfi.nextId()
  }
  return ('This thread did:\r' + doneDocs.join('\r'))
}

这里是代码:

const ParallelFileIterator = (function() {
  /**
  * Scans a folder, depth first, and returns a file at a time of the given mimeType.
  * Uses ScriptProperties so that this class can be used to process files by many threads in parallel.
  * It is the responsibility of the caller to tidy up artifacts left behind by processing threads that were timed out before completion.
  * This class will repeatedly dispatch a file until .completed(fileId) is called.
  * It will wait maxDurationOneFileMs before re-dispatching a file.
  * Note that Google Apps kills scripts after 6 mins, or 30 mins if you're using a Workspace account, or 45 seconds for a simple trigger, and permits max 30  
  * scripts in parallel, 20 triggers per script, and 90 mins or 6hrs of total trigger runtime depending if you're using a Workspace account.
  * Ref: https://developers.google.com/apps-script/guides/services/quotas
  maxDurationOneFileMs, mimeType, parentFolder=null, spawnFunction=null
  * @param {Number} maxDurationOneFileMs A generous estimate of the longest a file can take to process.
  * @param {string} mimeType The mimeType of the files required.
  * @param {Folder} parentFolder The top folder containing all the files to process. Only passed in by the first thread. Later spawned threads pass null (the files have already been listed and stored in properties).
  * @param {string} spawnFunction The name of the function that will spawn new processing threads. Only passed in by the first thread. Later spawned threads pass null (a trigger can't create a trigger).
  */
  class ParallelFileIterator {
    constructor(
      maxDurationOneFileMs,
      mimeType,
      parentFolder = null,
      spawnFunction = null,
    ) {
      log(
        'Enter ParallelFileIterator constructor',
        maxDurationOneFileMs,
        mimeType,
        spawnFunction,
        parentFolder ? parentFolder.getName() : null,
      )

      // singleton
      if (ParallelFileIterator.instance) return ParallelFileIterator.instance

      if (parentFolder) {
        _cleanUp()
        const t0 = Now.asTimestamp()
        _getPropsLock(maxDurationOneFileMs)
        const t1 = Now.asTimestamp()
        const { fileIds, fileRelativePaths } = _catalogFiles(
          parentFolder,
          mimeType,
        )
        const t2 = Now.asTimestamp()
        _setQueues(fileIds, [])
        const t3 = Now.asTimestamp()
        this.fileRelativePaths = fileRelativePaths
        ScriptProps.setAsJson(_propsKeyFileRelativePaths, fileRelativePaths)
        const t4 = Now.asTimestamp()
        _releasePropsLock()
        const t5 = Now.asTimestamp()
        if (spawnFunction) {
          // only triggered on the first thread
          const trigger = Trigger.create(spawnFunction, 1)
          log(
            `Trigger once per minute: UniqueId: ${trigger.getUniqueId()}, EventType: ${trigger.getEventType()}, HandlerFunction: ${trigger.getHandlerFunction()}, TriggerSource: ${trigger.getTriggerSource()}, TriggerSourceId: ${trigger.getTriggerSourceId()}.`,
          )
        }
        log(
          `PFI instantiated for the first time, has found ${
            fileIds.length
          } documents to process. getPropsLock took ${t1 -
            t0}ms, _catalogFiles took ${t2 - t1}ms, setQueues took ${t3 -
            t2}ms, setAsJson took ${t4 - t3}ms, releasePropsLock took ${t5 -
            t4}ms, trigger creation took ${Now.asTimestamp() - t5}ms.`,
        )
      } else {
        const t0 = Now.asTimestamp()
        // wait for first thread to set up Properties
        while (!ScriptProps.getJson(_propsKeyFileRelativePaths)) {
          Utilities.sleep(250)
        }
        this.fileRelativePaths = ScriptProps.getJson(_propsKeyFileRelativePaths)
        const t1 = Now.asTimestamp()
        log(
          `PFI instantiated again to run in parallel. getJson(paths) took ${t1 -
            t0}ms`,
        )
        spawnFunction
      }

      _internals.set(this, { maxDurationOneFileMs: maxDurationOneFileMs })
      // to get: _internal(this, 'maxDurationOneFileMs')

      ParallelFileIterator.instance = this
      return ParallelFileIterator.instance
    }

    nextId() {
      // returns false if there are no more documents

      const maxDurationOneFileMs = _internals.get(this).maxDurationOneFileMs
      _getPropsLock(maxDurationOneFileMs)
      let { pending, dispatched } = _getQueues()
      log(
        `PFI.nextId: ${pending.length} files pending, ${
          dispatched.length
        } dispatched, ${Object.keys(this.fileRelativePaths).length -
          pending.length -
          dispatched.length} completed.`,
      )
      if (pending.length) {
        // get first pending Id, (ie, deepest first)
        const nextId = pending.shift()
        dispatched.push([nextId, Now.asTimestamp()])
        _setQueues(pending, dispatched)
        _releasePropsLock()
        return nextId
      } else if (dispatched.length) {
        log(`PFI.nextId: Get first dispatched Id, (ie, oldest first)`)
        let startTime = dispatched[0][1]
        let timeToTimeout = startTime + maxDurationOneFileMs - Now.asTimestamp()
        while (dispatched.length && timeToTimeout > 0) {
          log(
            `PFI.nextId: None are pending, and the oldest dispatched one hasn't yet timed out, so wait ${timeToTimeout}ms to see if it will`,
          )
          _releasePropsLock()
          Utilities.sleep(timeToTimeout + 500)
          _getPropsLock(maxDurationOneFileMs)
          ;({ pending, dispatched } = _getQueues())
          if (pending && dispatched) {
            if (dispatched.length) {
              startTime = dispatched[0][1]
              timeToTimeout =
                startTime + maxDurationOneFileMs - Now.asTimestamp()
            }
          }
        }
        // We currently still have the PropsLock
        if (dispatched.length) {
          const nextId = dispatched.shift()[0]
          log(
            `PFI.nextId: Document id ${nextId} has timed out; reset start time, move to back of queue, and re-dispatch`,
          )
          dispatched.push([nextId, Now.asTimestamp()])
          _setQueues(pending, dispatched)
          _releasePropsLock()
          return nextId
        }
      }
      log(`PFI.nextId: Both queues empty, all done!`)
      ;({ pending, dispatched } = _getQueues())
      if (pending.length || dispatched.length) {
        log(
          "ERROR: All documents should be completed, but they're not. Giving up.",
          pending,
          dispatched,
        )
      }
      _cleanUp()
      return false
    }

    completed(fileId) {
      _getPropsLock(_internals.get(this).maxDurationOneFileMs)
      const { pending, dispatched } = _getQueues()
      const newDispatched = dispatched.filter(el => el[0] !== fileId)
      if (dispatched.length !== newDispatched.length + 1) {
        log(
          'ERROR: A document was completed, but not found in the dispatched list.',
          fileId,
          pending,
          dispatched,
        )
      }
      if (pending.length || newDispatched.length) {
        _setQueues(pending, newDispatched)
        _releasePropsLock()
      } else {
        log(`PFI.completed: Both queues empty, all done!`)
        _cleanUp()
      }
    }

    getFileRelativePath(fileId) {
      return this.fileRelativePaths[fileId]
    }
  }

  // ============= PRIVATE MEMBERS ============= //

  const _propsKeyLock = 'PropertiesLock'
  const _propsKeyDispatched = 'Dispatched'
  const _propsKeyPending = 'Pending'
  const _propsKeyFileRelativePaths = 'FileRelativePaths'

  // Not really necessary for a singleton, but in case code is changed later
  var _internals = new WeakMap()

  const _cleanUp = (exceptProp = null) => {
    log('Enter _cleanUp', exceptProp)
    Trigger.deleteAll()
    if (exceptProp) {
      ScriptProps.deleteAllExcept(exceptProp)
    } else {
      ScriptProps.deleteAll()
    }
  }

  const _catalogFiles = (folder, mimeType, relativePath = []) => {
    // returns IDs of all matching files in folder, depth first
    log(
      'Enter _catalogFiles',
      folder.getName(),
      mimeType,
      relativePath.join('/'),
    )
    let fileIds = []
    let fileRelativePaths = {}
    const folders = folder.getFolders()
    let subFolder
    while (folders.hasNext()) {
      subFolder = folders.next()
      const results = _catalogFiles(subFolder, mimeType, [
        ...relativePath,
        subFolder.getName(),
      ])
      fileIds = fileIds.concat(results.fileIds)
      fileRelativePaths = { ...fileRelativePaths, ...results.fileRelativePaths }
    }
    const files = folder.getFilesByType(mimeType)
    while (files.hasNext()) {
      const fileId = files.next().getId()
      fileIds.push(fileId)
      fileRelativePaths[fileId] = relativePath
    }
    return { fileIds: fileIds, fileRelativePaths: fileRelativePaths }
  }

  const _getQueues = () => {
    const pending = ScriptProps.getJson(_propsKeyPending)
    const dispatched = ScriptProps.getJson(_propsKeyDispatched)
    log('Exit _getQueues', pending, dispatched)
    // Note: Empty lists in Javascript are truthy, but if Properties have been deleted by another thread they'll be null here, which are falsey
    return { pending: pending || [], dispatched: dispatched || [] }
  }
  const _setQueues = (pending, dispatched) => {
    log('Enter _setQueues', pending, dispatched)
    ScriptProps.setAsJson(_propsKeyPending, pending)
    ScriptProps.setAsJson(_propsKeyDispatched, dispatched)
  }

  const _getPropsLock = maxDurationOneFileMs => {
    // will block until lock available or lock times out (because a script may be killed while holding a lock)
    const t0 = Now.asTimestamp()
    while (
      ScriptProps.getNum(_propsKeyLock) + maxDurationOneFileMs >
      Now.asTimestamp()
    ) {
      Utilities.sleep(2000)
    }
    ScriptProps.set(_propsKeyLock, Now.asTimestamp())
    log(`Exit _getPropsLock: took ${Now.asTimestamp() - t0}ms`)
  }
  const _releasePropsLock = () => {
    ScriptProps.delete(_propsKeyLock)
    log('Exit _releasePropsLock')
  }

  return ParallelFileIterator
})()

const log = (...args) => {
  // easier to turn off, json harder to read but easier to hack with
  console.log(args.map(arg => JSON.stringify(arg)).join(';'))
}

class Trigger {
  // Script triggering facade

  static create(functionName, everyMinutes) {
    return ScriptApp.newTrigger(functionName)
      .timeBased()
      .everyMinutes(everyMinutes)
      .create()
  }
  static delete(e) {
    if (typeof e !== 'object') return log(`${e} is not an event object`)
    if (!e.triggerUid)
      return log(`${JSON.stringify(e)} doesn't have a triggerUid`)
    ScriptApp.getProjectTriggers().forEach(trigger => {
      if (trigger.getUniqueId() === e.triggerUid) {
        log('deleting trigger', e.triggerUid)
        return ScriptApp.delete(trigger)
      }
    })
  }
  static deleteAll() {
    // Deletes all triggers in the current project.
    var triggers = ScriptApp.getProjectTriggers()
    for (var i = 0; i < triggers.length; i++) {
      ScriptApp.deleteTrigger(triggers[i])
    }
  }
}

class ScriptProps {
  // properties facade
  static set(key, value) {
    if (value === null || value === undefined) {
      ScriptProps.delete(key)
    } else {
      PropertiesService.getScriptProperties().setProperty(key, value)
    }
  }
  static getStr(key) {
    return PropertiesService.getScriptProperties().getProperty(key)
  }
  static getNum(key) {
    // missing key returns Number(null), ie, 0
    return Number(ScriptProps.getStr(key))
  }
  static setAsJson(key, value) {
    return ScriptProps.set(key, JSON.stringify(value))
  }
  static getJson(key) {
    return JSON.parse(ScriptProps.getStr(key))
  }
  static delete(key) {
    PropertiesService.getScriptProperties().deleteProperty(key)
  }
  static deleteAll() {
    PropertiesService.getScriptProperties().deleteAllProperties()
  }
  static deleteAllExcept(key) {
    PropertiesService.getScriptProperties()
      .getKeys()
      .forEach(curKey => {
        if (curKey !== key) ScriptProps.delete(key)
      })
  }
}

相关答案:https://dev59.com/62Yq5IYBdhLWcg3weQgy#68839699 - Rubén

0

正如许多人所提到的,解决这个问题的通用方法是在多个会话中执行您的方法。我发现这是一个常见的问题,我需要循环遍历一堆迭代,而我不想写/维护创建新会话的样板文件

因此,我创建了一个通用解决方案:

/**
 * Executes the given function across multiple sessions to ensure there are no timeouts.
 *
 * See https://dev59.com/62Yq5IYBdhLWcg3weQgy#71089403.
 * 
 * @param {Int} items - The items to iterate over.
 * @param {function(Int)} fn - The function to execute each time. Takes in an item from `items`.
 * @param {String} resumeFunctionName - The name of the function (without arguments) to run between sessions. Typically this is the same name of the function that called this method.
 * @param {Int} maxRunningTimeInSecs - The maximum number of seconds a script should be able to run. After this amount, it will start a new session. Note: This must be set to less than the actual timeout as defined in https://developers.google.com/apps-script/guides/services/quotas (e.g. 6 minutes), otherwise it can't set up the next call.
 * @param {Int} timeBetweenIterationsInSeconds - The amount of time between iterations of sessions. Note that Google Apps Script won't honor this 100%, as if you choose a 1 second delay, it may actually take a minute or two before it actually executes.
 */
function iterateAcrossSessions(items, fn, resumeFunctionName, maxRunningTimeInSeconds = 5 * 60, timeBetweenIterationsInSeconds = 1) {
  const PROPERTY_NAME = 'iterateAcrossSessions_index';
  let scriptProperties = PropertiesService.getScriptProperties();
  let startTime = (new Date()).getTime();

  let startIndex = parseInt(scriptProperties.getProperty(PROPERTY_NAME));
  if (Number.isNaN(startIndex)) {
    startIndex = 0;
  }

  for (let i = startIndex; i < items.length; i++) {
    console.info(`[iterateAcrossSessions] Executing for i = ${i}.`)
    fn(items[i]);

    let currentTime = (new Date()).getTime();
    let elapsedTime = currentTime - startTime;
    let maxRunningTimeInMilliseconds = maxRunningTimeInSeconds * 1000;
    if (maxRunningTimeInMilliseconds <= elapsedTime) {
      let newTime = new Date(currentTime + timeBetweenIterationsInSeconds * 1000);
      console.info(`[iterateAcrossSessions] Creating new session for i = ${i+1} at ${newTime}, since elapsed time was ${elapsedTime}.`);
      scriptProperties.setProperty(PROPERTY_NAME, i+1);
      ScriptApp.newTrigger(resumeFunctionName).timeBased().at(newTime).create();
      return;
    }
  }

  console.log(`[iterateAcrossSessions] Done iterating over items.`);
  // Reset the property here to ensure that the execution loop could be restarted.
  scriptProperties.deleteProperty(PROPERTY_NAME);
}

现在你可以轻松地像这样使用它:

let ITEMS = ['A', 'B', 'C'];

function execute() {
  iterateAcrossSessions(
    ITEMS,
    (item) => {
      console.log(`Hello world ${item}`);
    }, 
    "execute");
}

它将自动执行ITEMS中每个值的内部lambda函数,根据需要无缝地跨会话进行。

例如,如果您使用0秒的maxRunningTime,则会在4个会话中运行,并产生以下输出:

[iterateAcrossSessions] Executing for i = 0.
Hello world A
[iterateAcrossSessions] Creating new session for i = 1.

[iterateAcrossSessions] Executing for i = 1.
Hello world B
[iterateAcrossSessions] Creating new session for i = 2.

[iterateAcrossSessions] Executing for i = 2.
Hello world C
[iterateAcrossSessions] Creating new session for i = 3.

[iterateAcrossSessions] Done iterating over items.

这里有个变通方法真是太好了,但是周围有很多类似的“答案”。我不记得是否已经有一个好的规范问题了。 - Rubén

0

如果您是商业客户,现在可以注册App Maker的早期访问权限,其中包括灵活的配额

在灵活的配额系统下,这样的硬配额限制被取消。脚本在达到配额限制时不会停止。相反,它们会被延迟,直到有配额可用时,脚本执行才会恢复。一旦开始使用配额,它们将以固定速率重新填充。对于合理的使用,脚本延迟很少发生。


这个答案现在已经过时了。链接的页面已经不再可用。 - Rubén

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接