英文:
Design pattern for writing to a file from multiple async functions
问题
我有一个应用程序,尽可能经常将其状态保存到一个Json文件中。当多个事件同时发生时,它们可能会互相覆盖信息。状态由一个单例持有,我目前的解决方案是在保存到文件之前添加小的延迟,目前这样做可以工作,但我觉得它在某个时候可能会出问题。
英文:
I have an app where I save its state as often as possible to a Json file. When multiple events occur at the same time, they may overwrite information over each other. The state is held by a singleton, and my current solution is to add small delays before saving to the file, and it works for the time being but I feel like its waiting to go wrong at some point.
答案1
得分: 1
在将应用程序的“状态”保持在单例中的同时确实有效,但单例模式主要用于在可能创建新对象的情况下,它将始终返回最初创建的对象。即:不能创建第二个对象。
您的应用程序具有可能创建多个“状态”对象的能力,这似乎不是正确的架构。
相反,您可以将应用程序的“状态”对象创建为全局对象(不建议)或将其包装在命名空间文件中。这第二个选项将是首选。
将“状态”功能放在自己的文件中,这允许您有一个关于“状态”对象的单一来源。所有函数将包含在此模块中,然后在需要时由其他文件“required”以“get ()”“state”对象。另一种选择是使用Node.js“event”系统(仅在Electron主进程中)来调用“state”对象,类似于Electron的IPC系统。
一个重要的问题是:更新/保存请求是否比系统保存到磁盘更快?
如果应用程序“状态”保存仅在用户执行特定UI操作时才需要,那么只需要在发生任何这些特定事件之一时执行保存操作。由于用户不会在毫秒之间执行UI操作,因此模块可以无问题地执行请求(更新RAM中的“状态”对象,然后将“状态”对象保存到磁盘)。
如果应用程序“状态”可以由应用程序本身(而不是通过UI)快速连续更改,那么我们需要采取不同的方法。
在这种情况下,我会使用堆栈来跟踪请求。FIFO(先进先出)。如果要引用一个设计模式,我想“活动对象”设计模式接近您想要的。
这种功能的实现将是一个有命名空间的文件,其中包含“state”对象,一个堆栈计数器,一个“ready”(布尔)标志和一个基于承诺的(异步)函数来保存“state”对象到文件。还会有其他辅助函数,如堆栈增量和减量、切换“ready”标志以及公开可用的“update()”和“get()”函数。
该文件将在应用程序启动时从磁盘上读取“state”对象到RAM中。
当“state”对象更改(通过调用“update()”函数),您将将“ready”标志设置为false
,将对象推送到堆栈(数组)并增加堆栈计数器。然后,更新RAM中的“state”对象并将“state”对象保存到文件。当磁盘上的“保存”承诺得到满足时,从堆栈中删除对象,减少堆栈计数器,并将“ready”标志设置回true
。
与此同时,如果另一个请求进来以更新“state”对象,同样的事情会发生,但新的“state”对象将被推送到堆栈中,堆栈计数器将增加。这将继续进行,直到堆栈计数器下降到零,此时我们知道所有队列请求都已经处理完毕。
“ready”标志的目的是通知传入的“get ()”请求可能在内存中的内容与磁盘上的内容之间存在任何可能的差异。延迟“get ()”函数返回可能无效的“state”对象,直到“ready”标志为true
是明智的编码。也许可以使用一个基于承诺的机制来确保请求“state”对象的函数在“ready”标志为true
之前不会收到最新的更新。
如您所见,编写这种功能的强大模块确实需要一些工作,但第一次构建正确将确保它将在将来应对任何情况。
英文:
Whilst keeping the ‘state’ of your application in a singleton does work, the singleton pattern is primarily used for when the possibility of a new object is created, it will only and always return the initially created object. IE: A second object cannot be created.
Your application, having the ability to possibly create more than one ‘state’ object doesn’t sound like the correct architecture.
Instead, you can create the applications ‘state’ object as either a global object (not recommended) or as an object wrapped in a namespace file. This second option would be the preferred.
With 'state' functionality being in its own file, this allows you have a single source of truth for the 'state' object. All functions would be contained in this module and then required
by other files when the need arises to get()
the 'state' object. An alternative would be to use the Node.js event
system (only in the Electron main process) to call for the 'state' object, similar to Electrons IPC system.
An important question you need to ask is: Are the update / save requests coming in faster than the system can save to disk?
If application 'state' saves are only required when the user perform particular UI actions then you only need to perform a save operation when any of these particular events occur. As the user won't be performing UI actions milliseconds apart, the module can just perform the request (update the ‘state’ object in RAM and then save the ‘state’ object to disk) without issue.
If the application 'state' can be changed in quick succession by the application itself (and not via the UI), then we need to take a different approach.
In this instance, I would use a stack to track the requests. FIFO (first in, first out). If you want to reference a design pattern, I guess the 'active object' design pattern is close to what you want.
Implementation of such a function would be to have a name spaced file that contains the 'state' object, a stack counter, a 'ready' (Boolean) flag and a promise based (asynchronous) function to save the 'state' object to file. Other ancillary functions would be in there such as a stack increment and decrement, toggling the 'ready' flag and the publicly available 'update()' and get()' functions.
The file would initially read the 'state' object into RAM from disk upon application start.
When the 'state' object changes (via calling the 'update()' function), you would set the 'ready' flag to false
, push the object onto a stack (array) and increment the stack counter. Then, update the 'state' object in RAM and save the 'state' object to file. When the disk ‘save’ promise is fulfilled, remove the object from the stack, decrement the stack counter and set the 'ready' flag back to true
.
Whilst the above was going on, if another request came in to update the 'state' object, the same would occur but the new 'state' object would be pushed onto the stack and the stack counter incremented. This would continue to go on until the stack counter came back down to zero, at which point we know that all queue requests have now been processed.
The point of the 'ready' flag is to inform an incoming get()
request of any possible discrepancy between what is in memory and what is on disk. Delaying the get()
function from returning a possibly invalid ‘state’ object until the ‘ready’ flag is true
would be prudent coding. Perhaps a promise based mechanism could be used to ensure the function requesting the ‘state’ object does not receive the latest update until the ‘ready’ flag is true
.
As you can see, to code a robust module of this functionality does require some work, but building it right the first time will ensure it will hold up to anything you throw at it in the future.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论