【问题标题】:JavaScript - How to save the state on exit when detecting CTRL+C SIGINTJavaScript - 如何在检测到 CTRL+C SIGINT 时保存退出状态
【发布时间】:2023-01-02 08:06:04
【问题描述】:

我正在开发一个执行一些计算的 NodeJS 应用程序。当应用程序启动时,它能够从文件加载数据以从中断处继续。我还没有想出在获得中断信号时将数据保存到文件的最佳方法,这表明应用程序应该完成计算并关闭。

这是我到目前为止的简化版本:

const filesystem = require('fs');

process.on('SIGINT', function onSigInt() {
    console.log('SIGINT ', new Date().toISOString());
    shutdown();
})

// This function should somehow save the data to file
function shutdown() {
    process.exit();
}

async function run() {

    let allData = [];

    // Load existing database from file, if exists
    try {
        const fileData = filesystem.readFileSync('output.json', {encoding:'utf-8', flag:'r'});
        allData = JSON.parse(data.toString());
        console.log(`Loaded {allData.length} records from file`);
    }
    catch (err) {
        console.log('No file to load data from, continuing with empty dataset');
    }

    while(True) {
        doCalculation(allData);
    }

    // If we run out of things to calculate, and the infinite loop above
    // ends, then we save data to file like this:
    filesystem.writeFileSync('output.json', JSON.stringify(allData, null, 4));
}

包含数据集的对象是allData。我需要将其设为全局变量吗?这似乎是最明显的解决方案。我是 JavaScript 的新手 - 可能有另一种在 JS 中做事的方法?

【问题讨论】:

    标签: javascript


    【解决方案1】:

    我不是 javascript 大师,但是是的,我认为为您的用例设置一个全局变量应该很好。很乐意阅读其他建议

    如果可能的话,另一种方法是将 while 循环条件保留为全局变量,并在处理 SIGINT 信号时将其设置为 false。当 while 循环退出时,您的文件将充满来自您的工作流的已处理数据。

    如果它适合你,请继续使用任何方法 :) 至于可读性,我觉得还不错


    以下部分比较长,更多的是出于好奇

    我目前正在探索的另一种有趣的方法(个人品味)是使用generator functions,它或多或少类似于 python 协程。

    对我来说,你的问题引发了一些关于长时间运行 CPU 绑定操作和任务取消的有趣想法。 所以我会很心疼^^'

    如果您是 javascript/nodejs 的新手,我希望语法不会让您气馁。我将使用一些标准函数,如setTimeoutsetIntervalsetImmediate和javascript的Promises


    对于 cpu bounded function 的情况,单线程事件循环将被阻塞,无法处理其他事件,如 process.on('SIGINT',callback) 或其他。通常对于此用例,您有多种选择:

    1. 工作线程或子进程

    2. 分布式任务队列系统,如bull

    3. 以块的形式处理您的计算,并在某个时候调用 setImmediate,提供将在事件循环的下一次迭代中稍后执行的回调。

    4. 请告诉我,我很乐意学习新事物 :)

      对于像您这样的一项大任务,我认为前两种方法会有点矫枉过正,尽管如果某些块不相关,您可能可以在多个线程/进程之间拆分数据。

      第三个选项可能对 setImmediate 很有趣,但放置回调和维护已完成的范围通常很麻烦。

      通过函数生成器和 setImmediate 的组合,我们可以在操作的各个点产生长时间运行的函数,函数在产生时的某个点停止,我们调用 setImmediate 让事件循环处理其他事件。完成后,我们可以再次推进长时间运行的函数,直到另一个屈服点。

      我们可以重复这个循环,直到长时间运行的函数完成,或者我们可以拦截一些告诉我们停止长时间运行的函数的事件。

      举个例子,希望更清楚。

      /* Your UpperBound, you can increase it, 
         the function with yield will be of course slower
      */
      const UPPERBOUND = 10 
      
      //Plain bigCalculation loop
      function bigCalculation() {
          let count = 0;
          for (let i = 0; i < UPPERBOUND; i++) {
              count += i;
          }
      
          return count
      }
      
      // Function generator, to yield the execution at some point
      function* bigCalculationYielder() {
          let count = 0;
          for (let i = 0; i < UPPERBOUND; i++) {
              count += i;
              yield count // the yield to suspend the current loop.
      
      
              /*
                conditonal yielding when count is a modulo of 100,
                for better performance or use cases
                if(count %100){
                  yield count
                }
              */
      
      
          }
          return count
      }
      
      function yieldCalculation() {
          const calculationYielder = bigCalculationYielder() // get the generator()
          function loop() {
             /*
              Calling next on the generator progress the function 
              until the next yield point
              */
              const iteration = calculationYielder.next() 
              
              console.log(iteration) 
              
              // When the iteration is done, we can quit the function
              if (iteration.done) {
                  clearInterval(id) // stopping the setInterval function
                  return
              }
      
              // Shorter way
              //setImmediate(()=>{loop()} 
              
      
              setImmediate(() => {
                  // set a litlle time out to see the log pin from the set interval
                  return setTimeout(() => loop(), 50) 
              }) // The set immediate function will make progress on the event loop
          }
          return loop()
      }
      
      // A setInterval to see if we can interleave some events with a calculation loop
      const id = setInterval(() => console.log("ping"), 50) 
      const task = yieldCalculation()
      
      /*You can increase the UPPERBOUND constant and use the classic
       bigCalculation function. Until this function end, you won't see the 
       setInterval ping message
      */
      

      在此示例中,我们尝试将函数的进度与从 setInterval 接收的事件交织在一起。

      我们可以调用 generator.next() 来进行计算。 如果计算完成,我们清除setInterval和定时器并返回函数,否则调用setImmediate,nodejs可以处理其他事件并再次回调循环函数完成计算。


      这种方法也适用于 Promise, Promise 比回调好一点。您可以在 Promise 中定义一些工作,一旦它解决(成功结束函数),您可以使用 .then 链接操作以获得承诺的结果并对其进行处理。 Promise 通常与异步操作一起使用,但它非常灵活。

      const promise=new Promise((resolve,reject)=>{
          let i=100;
          setTimeout(()=>{
              resolve(i)
              return;
          },2000); // We wait 2 seconds before resolving the promise
      })
      
      console.log(promise) // The promise is pending
      
      promise.then(val=>console.log("finish computation with : ",val)) 
      /* once 2 secondes ellapsed, we obtain the result 
      which been declared in resolve(), inside the promise 
      */
      

      从前面带有函数生成器的示例中,我们可以让我们的 yieldCalculation 函数返回一个承诺。 (我改了名字不好意思)

      只有当我们完成大计算时,我们才能解决它并用 then 链接它

      const UPPERBOUND = 10
      
      function* bigCalculationYielder() {
          let count = 0;
          for (let i = 0; i < UPPERBOUND; i++) {
              count += i;
              yield count
          }
          return count
      }
      
      function yieldHandler() {
          const calculationYielder = bigCalculationYielder() 
          
          /* this time we return a promise, once it the iteration is done
             we will set the value through resolve and return 
          */
          return new Promise((resolve, reject) => {
              function loop() {
      
                  const iteration = calculationYielder.next() 
                  console.log(iteration)
                  
            
                  if (iteration.done) {
      
                      // you are setting the value here 
                      resolve(iteration.value) 
                      return
                  }
                  //setImmediate(()=>{loop()})
                  setImmediate(() => {
                      return setTimeout(() => { loop() }, 50)
                  })
      
              }
      
              loop()
          })
      
      }
      
      const id = setInterval(() => console.log("ping"), 50)
      const task = yieldHandler()
      console.log(task)
      
      /* When the computation is finished, task.then will be evaluated
         and we can chain other operations
      */
      task.then(val=>{console.log("finished promise computation with : ",val); clearInterval(id)}) 
      

      通过这些示例,我们看到我们可以在多个块中执行“长”操作,并让 nodejs 处理其他事件。 Promise 对于操作链来说更好一些。

      对于您的用例,我们缺少两部分:

      • 如何处理 SIGINT 信号以中断长时间运行的操作

      • 如何在长时间运行的操作完成或被中断后保存文件

      在最后一个示例中,我将创建一个任务类,以处理函数生成器的运行循环,它还将“拦截”SIGINT 信号。

      该示例将创建一个 json 对象,其形式为:{datas:[ {a,b} , {a,b} , {a,b} , {a,b} ,...]} 并将其写入文件

      我们走吧 !

      "use strict";
      // used for class declaration, it is javascript strict mode
      
      const fs = require('fs')
      
      // Depending on how fast is your machine
      // you can play with these numbers to get a long task and see cancellation with SIGINT
      const UPPERBOUND_TASK=10000
      const UPPERBOUND_COMPUTATION=10000
      
      // An async generator
      // Not usefull here but can be if you want to fetch data from API or DB
      async function* heayvyTask() {
        let jsonData = { datas: [] };
        let i=0;
      
        while (true) {
          
          if(i==UPPERBOUND_TASK){
            break
          }
          heavyComputation(jsonData)
          i++
      
          // We yield after the headyComputation has been process.
          // Like that we can fill the data by chunck and prevent from yielding too much
          yield jsonData
          
        }
        return jsonData
      }
      
      // The effective process.
      // We populate the jsonData object
      function heavyComputation(jsonData) {
        for (let i = 0; i < UPPERBOUND_COMPUTATION; i++) {
          const data = { a: i, b: i + 1 }
          jsonData.datas.push(data)
        }
      }
      
      // Saving the data to a local file
      function saveDataToFile(jsonData) {
        console.log(jsonData.datas.length)
        console.log("saving data to file")
        fs.writeFileSync("test.json", JSON.stringify(jsonData))
        console.log("done")
      }
      
      class Task {
        constructor(process) {
      
          //heayvyTask function
          this.process = process
      
          this.cancelTask = false
        }
        start() {
          
          // We are getting the heayvyTask function generator
          const process = this.process()
          
          
          return new Promise(async (resolve, reject) => {
            try {
              // Declaration of the loop function
              async function loop() {
      
                // Here we are using an async function generator
                // So we have to await it
                // It can be usefull if you peform async operation in it
                // Same as before your are running the function till the next yield point
                const val = await process.next()
      
                // If the generator function completed
                // We are resolving the promise with jsonData object value
                if (val.done) {
                  console.log("task complete")
                  resolve(val.value)
                  return
                }
      
                // If the task has been canceled
                // this.cancelTask is true and we resolve the promise
                // All the data handled by the generator will be pass to the promise.then() 
                if (this.cancelTask) {
                  console.log("stopping task")
                  resolve(val.value)
                  return
                }
                
                // Crazy looping
                setImmediate( ()=>{
                  work()
                })
              }
      
              // We use bind to pass the this context to another function
              // Particulary, we want to access this.cancelTask value
              // It is related to "this scope" which can be sometimes a pain
              // .bind create an other function   
              const work=loop.bind(this)
              
              // Effectively starting the task
              work()
            }
            catch (e) {
              reject(e)
              return
            }
          })
        }
      
        // We want to cancel the task
        // Will be effetive on the next iteration
        cancel() {
          this.cancelTask = true
          return
        }
      }
      
      /* We create a task instance
      The heavytask generator has been pass as an attribute of the Task instance
      */
      let task = new Task(heayvyTask);
      
      // We are running the task.
      // task.start() returns a promise
      // When the promise resolves, we save the json object to a file with saveDataToFile function
      // This is called when the calculation finish or when the task has been interupted
      task.start().then(val => saveDataToFile(val)).catch(e=>console.log(e))
      
      // When SIGINT is called, we are cancelling the task
      // We simply set the cancelTask attribute to true
      // At the next iteration on the generator, we detect the cancellation and we resolve the promise
      process.on('SIGINT',()=>task.cancel())
      

      基本流程是:

      • 有SIGINT信号

      任务实例 -> 运行任务 -> SIGINT -> 解决承诺 -> 将数据保存到文件

      • 跑到最后

      任务实例 -> 运行任务 -> 生成器函数结束 -> 解决承诺 -> 将数据保存到文件

      在所有情况下,一旦承诺得到解决,您的文件将以相同的方式保存。

      在性能方面,带有 yelding 的生成器函数当然更慢,但它可以提供某种协作,在单线程事件循环上并发,这很好并且可以利用有趣的用例。

      使用 setImmediate 循环似乎没问题。我在堆堆栈溢出方面遇到了一些错误,但我认为这与数组结尾太大有关。 如果字符串结束时太大,我也会遇到一些问题,当将它保存到文件时

      对于调用堆栈,以递归方式调用循环,setImmediate 看起来很适合它,但我没有调查太多。


      在不进一步了解 doCalculation 函数的情况下,我只能建议将屈服点放在有意义的地方。 如果计算遵循某种交易风格,也许在它结束时。否则,你可以有几个屈服点。 在调用 return 语句之前,循环将进一步推动生成器函数

      如果您对使用生成器函数的长时间运行的任务感到好奇,this project 似乎提供了一些不错的 API。我没有玩它,但文档看起来不错。


      编写和使用它很有趣,如果它能有所帮助,那就太好了

      干杯!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-09-06
      • 2011-02-23
      • 1970-01-01
      • 2016-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多