-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement storage limits for local repository #159
Comments
while working on the BlockStore API, I found that current code doesn't use delBlock at all, meaning that local store can only grow :) So I think it's also important feature for any practical usage, no less that SQLite backend. |
Yes, this is definitely an important feature for a usable product, so this should be relatively high on our priority list. |
Yes, all issues that currently in our priority list are assigned to me or @michaelsbradleyjr, so we will take care of it. If this simple approach will go out of control, we can create milestone for them. |
I have started researching some possibilities. For The
The result is "the total amount of disk space used" in bytes. Note that "total" doesn't include "Freelist pages, pointer-map pages, [or] the lock page", so numbers reported by e.g. We could periodically query the That would require some custom queries, but nim-datastore exports the necessary fields and helpers so that it's entirely doable. We would need to do some experimentation in relation to I don't have an idea yet for the best approach for I'm not aware of any general cross-platform solution for a percentage-based quota. For the platforms we primarily target (Linux, macOS, Windows) we can probably shell out and use cli utilities in |
Doesn't SQLite provide some native facility to limit the DB size? |
even if it does, we need solution deleting LRU blocks rather than hard limit that will just prevent adding new data to DB. |
Taking another crack at this from an API perspective. This is a sketch, not getting into details of how to deal with cli params, whether the API should be What if for method quota(self: BlockStore, percentage: Percentage): Future[?!int]
method quota(self: BlockStore, bytes: int): Future[?!int] Where the return value is the number of bytes over/under quota. In the case of In the case of How the return value is calculated would depend on the implementation of And we could also have method purge(self: BlockStore, bytes: Positive, delta: Percentage): Future[?!Natural] Where the return value is the number of bytes actually purged. The The How the It would be expected that For |
Something to keep in mind: there are also blocks that we should keep around because they're part of a contract that we're engaged in. Not sure whether these should go into a separate store, or that the store itself should treat them differently. |
Yeah, contracts and these restrictions contradict, so we may choose between:
Also, we essentially have two parts in blockstore, and their sizes probably should be regulated independently:
|
The Sales module only sells storage that it's been told that it is available. So we only get into contracts for disk space that we explicitly marked as available. So you can let contracts know about the restrictions. |
But for the starter, let's develop the system ignoring contracts problem. I think we can use any of three approaches:
As far as we have "last access time" for each block, we can run DELETE queries requesting to delete N blocks with oldest timestamps. I think this solves simplfied (contract-ignoring) problem for SQLite backend. So, the algorithm is:
Since SQLite is inherently single-threaded system (AFAIK), we can still have too large delay on a single mass-DELETE operation. It may be preferable to delete records in smaller batches. Actually, to distribute load evenly, we can each time delete only a few records, and even just a signle one in extremum. So, alternative algorithm:
|
@markspanbroek I think about adding to BlockStore a request Sidenote: we can have such checks with other subsystems too (CPU, Network and disk bandwidth required for PoRs). |
I would rather have a way to reserve space in the BlockStore. I expect a user would want to dedicate an amount of storage for sale explicitly. There's a REST API endpoint for doing that in the codex node already. When the user indicates that it wants to sell e.g. 100 Gb, the store should always keep this space available for contracts to use. |
For now we simply need something that will refuse to write past a specific limit, without doing any automatic deletion or cleanup. What I would like to see as a first step is:
|
Thats correct, I want the minimum possible set of functionality right now, we can evolve this iteratively. In particular, it should be fine to stop storing new blocks once we reach the limit. |
In particular this means that ECC will became useless - we rely on storing decoded data in the local store. |
How so? We're only talking about limiting storage use, nothing else. Sure some files would not be downloadable in full due to size, but this is already true - you can't download more than the size of the hard drive. I would like to avoid all the complexity associated with "hot" pruning (client is running) for now and focus on a simple and concrete task. We'll come back to this once we have a better understanding of how the software behaves and what sort of real requirements we are working with. As a second step, we can do "cold" pruning (client is stopped), which should give us a good understanding of what sort of issues we're going to be dealing with once we move to "hot" pruning. |
Recall this discussion about
So, once we stopped saving data from network to local store, we may get very slow downloads and ECC will definitely stop working. Prefetcher/decoder overall rely on our ability to store entire file in cache. |
Yes, thats expected, I never meant this issue to handle more complex cases than simple limits on the total amount of bytes the repo can occupy. We can live with some level of degraded functionality once we have reached our storage quota, the alternative being simply stopping the process. Doing proper hot pruning is not an easy task which overall buys us very little at this point. We should improve it, but I don't see any pressing need to do it immediately. |
From discussion with Dmitry:
@emizzle will it work for you? |
I'm not sure if this As @markspanbroek pointed out, currently a host specifies how much space they want to make available to sell (via rest api). This "availability" is modified by the sales module, such that when a contract is started, the availability is removed (even if the availability was greater than the amount consumed in the contract -- something we probably should optimise later) and when the contract has finished, the availability is added back. In either case, once availability is added by the host (via rest api), perhaps the sales module should inform the datastore to "reserve" this space to not be consumed? What do you think @Bulat-Ziganshin? The flow could be something like this:
|
first, probably these new REST APIs should be documented. It's up to you, just a side thought. Check Dmitry's post before mine: #159 (comment) He proposed to implement a very simple approach - we configure maximum disk usage by BlockStpore, and once the quota is reached, we just stop writing to the store. The obvious idea behind that is just to allow a node operator to set a hard limit of disk usage by Codex. As he said, it's just a quick-and-dirty solution. And it was better to implement for the October release. Your REST API is already more demanding:
I tried to discuss here the extended variant of feature with Mark - before Dmitry said that we need to implement just the basic one. My thought - let's implement the basic one for the starter, its implementation is anyway first half of the implementation of the extended feature, plus it has more chances to become implemented prior to October release. |
Understood. Keeping it simple for now for phase 1 with iterations later sounds like a very reasonable approach. Perhaps this leaves us with two options:
In my opinion, option 1 is the simplest for now, however option 2 is most similar to how the flow should be later on. |
@emizzle EDIT: Yeah, I finally got it. You are right - let's do it in the way you proposed. Also, I would like to modify putBlock API now, so we can distinguish between calls for external and paid blocks - in order to not bother you later with this change. |
Sounds good. I actually don't think any contract changes are required, it would only be a call to the "stupid" |
I tried to find how contracts use putBlocks and didn't find anything - in the entire program putBlocks is called in 3 places - erasure.nim, block exchange for blocks received from network, and node.store() called by upload REST API |
That's correct. Contract interaction with the blockstore is currently limited to the
|
I don't think this is the correct approach. The functionality I have in mind is as follows - the user sets a quota that the node is allowed to consume, the node will return how much of that quota is still available for sale. Also, we should not conflate caching blocks with persisting blocks, they might use the same underlying machinery (i.e. repo abstraction), but they should be configurable separately. The difference between the two is:
What I'm proposing as a stopgap solution, is to implement the basic
To support (1) and avoid lengthy scans of the repo to calculate the total used byte, we should keep track of how many bytes have been written and/or evicted to/from it. This can be recorded under Full functionality extensionsFor the most part, both the caching and persistent quotas can be supported by the same underlying implementation. My current thinking is that we keep some metadata per block to support caching and persisting blocks. We need to keep track of the last time the block was accessed so we can evict it from the caching store and we need a flag that indicates wether the block should be counted as part of the The algorithm for storing blocks in the repo looks something like the following:
Edits:
|
Makes sense. We can update the rest api endpoint to check if there's enough persistent quota available in the datastore.
@dryajov, will there be a datastore api that allows us to check the amount of persistent storage remaining? |
For the FSStore DataStore it is pretty easy to reserve space by creating a RESERVE file of 100 GiB (or whatever). Then you shrink that file as you add real chunks and grow it as you purge and we can ensure we can always honor the contract. The reserve file size makes implementing This seems like a simple, serviceable & OS portable quota system. Most of us and even many non-expert users have a lot of experience filling filesystems to 100% and then recovering usable space by deleting/truncating files - successfully even if there are not competing writers getting intermittent ENOSPCes. So, I think this, too, could work and even with users pushing things to the brink and returning from said brink. { Some, but maybe not all subtleties: For our code we can block writes, but the FS can fill after we shrink RESERVE but before we use it, but selling against an 99+% full FS with alien writers is insoluble. To reserve space, we need to either For SQLiteStore, big questions arise some of which @michaelsbradleyjr starts.."Does (A user with admin-ish rights on Unix can make an "FS within a file" to at least keep |
I was thinking that for the purpose of deciding when to delete blocks, there could be no difference between cached and contract blocks: Cached blocks are kept for a short time in accordance with the caching settings. Contract blocks are kept for a longer but still limited time in accordance with the contract. I was thinking the expiration datetime for a contract block should be the duration of the contract + some grace period (hours?, days?, configurable). I am unsure about whether the cached blocks and contract blocks can be treated identically when it comes to quotas. Do we want a max quota for the entire node, or one quota for caching and another for contract storage? Additionally, I like the idea of marking a block as non-persistent when the contract has expired. Currently, we delete blocks as soon as their expiration date has been reached. Perhaps we could consider a strategy of "don't delete unless you have to", where we fill up the quota on purpose and start cleaning the oldest, least interesting, non-persistent (any more) blocks only when we have no other way to respect the quota? Downsides: Write performance goes down because we may need to quickly make some room. It becomes trickier to answer the question how much space is available. |
Some of the questions raised in this issue have already been addressed by the
This is how it works already, the
This might be a nice addition, a grace period certainly makes sense in some circumstances...
The The flow is super simple, there is a |
Currently, the local repository doesn't implement any storage limits and it's possible to fill up the entirety of the users drive.
We need to add:
%
of bytes available to codexThe text was updated successfully, but these errors were encountered: