Conversation
5e5928a to
45ebcfe
Compare
4ac3464 to
31f431b
Compare
fd45659 to
d7053f7
Compare
cbc430e to
d7b246a
Compare
d7053f7 to
ac896f9
Compare
d7b246a to
f10aa90
Compare
f10aa90 to
981332f
Compare
ac896f9 to
532ceda
Compare
44e1718 to
d2666ac
Compare
README.md
Outdated
| -t, --default-ttl Default dataset expiry in seconds [=$DefaultDefaultExpiry]. | ||
| --maint-interval Maintenance interval in seconds - determines frequency of maintenance cycle: | ||
| how often datasets are checked for expiration and cleanup. Value 0 disables the | ||
| maintenance [=$DefaultMaintenanceInterval]. |
There was a problem hiding this comment.
Don't seem like it's resolving the default here
There was a problem hiding this comment.
You're right. I will change it to the literals 86400 (24 hours) for ttl and 300 for maintenance interval (5 minutes).
There was a problem hiding this comment.
I think this used to work fine (in fact previous versions of codex do resolve this fine), however, I'm seeing the same in #700. Looks like Duration isn't properly stringified.
README.md
Outdated
| often blocks are checked for expiration and cleanup | ||
| [=$DefaultBlockMaintenanceInterval]. | ||
| --block-mn Number of blocks to check every maintenance cycle [=1000]. | ||
| -t, --default-ttl Default dataset expiry in seconds [=86400]. |
There was a problem hiding this comment.
As mentioned, this should actually work with the constants, which is preferable and it appears like it broke some time ago, so a better solution would be to leave the constants in for now and figure out why they aren't resolving. We don't have to hold this pr because of it however.
codex/node.nim
Outdated
|
|
||
| # Retrieve all blocks of the dataset sequentially from the local store or network | ||
| trace "Creating store stream for manifest", cid | ||
|
|
There was a problem hiding this comment.
Why do we need this here, is it to prevent retrieving expired datasets?
There was a problem hiding this comment.
No, whenever we store a new dataset we need to explicitly call trackExpiry so that all blocks within that dataset will get maintained.
dryajov
left a comment
There was a problem hiding this comment.
Lets get this merged with safe-block-deletion and I'll give it a more thorough review. There lots of changes across main and this two branches which makes reviewing this separately a bit hard.
fa36cd4 to
a951bbf
Compare
abb6a12 to
4cf5cd0
Compare
benbierens
left a comment
There was a problem hiding this comment.
Docker image of this branch seems to be passing basic dist-tests.
codex/codex.nim
Outdated
| wallet = WalletRef.new(EthPrivateKey.random()) | ||
| network = BlockExcNetwork.new(switch) | ||
|
|
||
| metaDs = SQLiteDatastore.new(config.dataDir / CodexMetaNamespace) |
There was a problem hiding this comment.
metaDs seems to be defined again on line 262, but then as a LevelDbDs.
There was a problem hiding this comment.
Thanks for catching it 👌 We should be using a single metaDb
|
|
||
| CodexProof.decode(bytes) | ||
|
|
||
| func `%`*(proof: CodexProof): JsonNode = % byteutils.toHex(proof.encode()) |
There was a problem hiding this comment.
I don't see how the changes in this file are connected to the expiry per dataset. A quick explain will do! :D
There was a problem hiding this comment.
This whole change includes changing of the type of quota usage (used, reserved and available bytes) from simple uint to NBytes. Then to avoid converting it to uint everywhere I changed whenever it was suitable also to NBytes, that includes the data model used for REST endpoint, the RestRepoStore object type. And since it's need to be serialized properly on the endpoint we need such encoder.
codex/stores/maintenance.nim
Outdated
| if err =? (await self.recordCheckpoint(treeCid, datasetMd)).errorOption: | ||
| return failure(err) | ||
|
|
||
| return success() |
There was a problem hiding this comment.
On finishing a successful delete of a dataset, should we delete that dataset's entry in the dataset-metadata datastore? Is this already handled somewhere? or is there a reason not to?
There was a problem hiding this comment.
Yeah, we do it when recording a checkpoint - we check if progress reached 100% and if so we remove the dataset metatada. Line 198.
codex/stores/maintenance.nim
Outdated
| return success() | ||
| else: | ||
| datasetMd.checkpoint.progress = index | ||
| return await self.deleteBatch(treeCid, datasetMd) |
There was a problem hiding this comment.
Should this call itself, does this not risk huge callstacks for large datasets? Might we not instead 'simply' wait for the next cycle of superviseDatasetDeletion to delete more blocks?
There was a problem hiding this comment.
No, recursion is trampolined in Future. Basically you have infinite™️ callstack with {.async.}
| quotaMaxBytes: quotaMaxBytes, | ||
| blockTtl: blockTtl | ||
| ) | ||
| export store, types, coders |
There was a problem hiding this comment.
Splitting stuff up?! I like it. 👍
8b6d782 to
61617b8
Compare
deec57d to
4d39a5c
Compare
codex/stores/maintenance.nim
Outdated
| except Exception as exc: | ||
| error "Unexpected error during maintenance", msg = exc.msg |
There was a problem hiding this comment.
Exception should not be caught, because Defect is not catchable which is a derived type of Exception. Instead, use what was there previously:
except CancelledError as error:
raise error
except CatchableError as exc:There was a problem hiding this comment.
Also, there is a combination of exceptions being caught and errored Results being handled, which indicates there are some exceptions leaking in the underlying context, when they should be returned as an errored Result.
Ideally we should mark all of the routines that return a Result in the underlying context with {.raises:[].}. When the chronos v4 changes go in, we can also mark the async procs with {.async: (raises:[]).}
codex/stores/maintenance.nim
Outdated
| self.offset = 0 | ||
| if (datasetMd.expiry < self.clock.now) and | ||
| (datasetMd.checkpoint.timestamp + self.retryDelay.seconds < self.clock.now): | ||
| asyncSpawn self.superviseDatasetDeletion(treeCid, datasetMd) |
There was a problem hiding this comment.
Possibly we should track these futures using TrackedFutures so that we can
successfully cancel them on stop:
DatasetMaintainer* = object
trackedFutures: TrackedFutures
proc new*(
T: type DatasetMaintainer,
# ...
) =
DatasetMaintainer(
#...
trackedFutures = TrackedFutures.new()
#...
)
# Usage:
proc checkDatasets(self: DatasetMaintainer): Future[?!void] {.async.} =
# ...
discard self.superviseDatasetDeletion(treeCid, datasetMd).track(self)
# ...
proc stop*(self: DatasetMaintainer): Future[void] {.async.} =
await self.trackedFutures.cancelTracked()There was a problem hiding this comment.
Good idea, adding it 👍
codex/stores/maintenance.nim
Outdated
| await modify[DatasetMetadata](self.metaDs, key, | ||
| proc (maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | ||
| if currDatasetMd =? maybeCurrDatasetMd: | ||
| let datasetMd = DatasetMetadata( | ||
| expiry: max(currDatasetMd.expiry, minExpiry), | ||
| leavesCount: currDatasetMd.leavesCount, | ||
| manifestsCids: currDatasetMd.manifestsCids, | ||
| checkpoint: currDatasetMd.checkpoint | ||
| ) | ||
| return datasetMd.some | ||
| else: | ||
| raise newException(CatchableError, "DatasetMetadata for treeCid " & $treeCid & " not found") | ||
| ) |
There was a problem hiding this comment.
This is a bit more readable (for me, at least)
| await modify[DatasetMetadata](self.metaDs, key, | |
| proc (maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | |
| if currDatasetMd =? maybeCurrDatasetMd: | |
| let datasetMd = DatasetMetadata( | |
| expiry: max(currDatasetMd.expiry, minExpiry), | |
| leavesCount: currDatasetMd.leavesCount, | |
| manifestsCids: currDatasetMd.manifestsCids, | |
| checkpoint: currDatasetMd.checkpoint | |
| ) | |
| return datasetMd.some | |
| else: | |
| raise newException(CatchableError, "DatasetMetadata for treeCid " & $treeCid & " not found") | |
| ) | |
| proc modifyData(maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | |
| without currDatasetMd =? maybeCurrDatasetMd: | |
| raise newException(CatchableError, "DatasetMetadata for treeCid " & $treeCid & " not found") | |
| let datasetMd = DatasetMetadata( | |
| expiry: max(currDatasetMd.expiry, minExpiry), | |
| leavesCount: currDatasetMd.leavesCount, | |
| manifestsCids: currDatasetMd.manifestsCids, | |
| checkpoint: currDatasetMd.checkpoint | |
| ) | |
| return datasetMd.some | |
| await modify[DatasetMetadata](self.metaDs, key, modifyData) |
I still don't think we should be raising exceptions here, because the underlying
implementations (defaultModifyImpl and defaultModifyGetImpl) simply try/except these and turn them into Results.
There was a problem hiding this comment.
If we want a modify operation to be stopped, raising an exception is the only way to do that. In this case we want to stop it. The rest of the flow goes as expected, exception gets turned into result and everything gets eventually logged on an error level.
As for the first part of the comment I can extract that anonymous proc into a named proc if you want, however I don't see how it automatically becomes more readable this way.
There was a problem hiding this comment.
If we want a modify operation to be stopped, raising an exception is the only way to do that. In this case we want to stop it.
You say this because of the contract for modifyGet, right? Cause there is currently no provisioning there for a modify operation to be aborted?
I suppose returning maybeCurrDatasetMd would be equivalent to a NOP, but sort of inefficient as it would still trigger a write to the underlying store?
There was a problem hiding this comment.
If we want a modify operation to be stopped, raising an exception is the only
way to do that
With Dmitriy's suggested change, returning a Result will be the way to stop an
operation, which is exactly what I had in mind.
I can extract that anonymous proc into a named proc if you want, however I
don't see how it automatically becomes more readable this way.
Understood. As an outside reader, I thought perhaps you might want to know what
is considered subjectively "more readable" for that reader.
There was a problem hiding this comment.
You say this because of the contract for modifyGet, right? Cause there is currently no provisioning there for a modify operation to be aborted?
It's not mention in the docs, but in the signature, we return Future which implies it can be a failure.
I suppose returning maybeCurrDatasetMd would be equivalent to a NOP, but sort of inefficient as it would still trigger a write to the underlying store?
Yep, returning the original argument is NOP. So the state in datastore is essentially the same as raising error. The difference is that when error is raised from a closure, that error is propagated to the caller of modifyGet as failure(err). Probably it should be documented.
There was a problem hiding this comment.
With Dmitriy's suggested change, returning a Result will be the way to stop an
operation, which is exactly what I had in mind.
Future already communicates the error. With Future[Result[T]] it becomes ambiguous where the error will be.
Also not sure why are we even talking about it. I'm not changing anything there.
codex/stores/maintenance.nim
Outdated
| if err =? (await self.recordCheckpoint(treeCid, datasetMd)).errorOption: | ||
| return failure(err) |
There was a problem hiding this comment.
Why not simply delete the checkpoint here instead of including the delete logic
in the modify callback?
There was a problem hiding this comment.
Most of the time we're updating the checkpoint here. Deletion happens conditionally only as a last step of the process (recording 100% progress is equivalent to deleting the checkpoint along with dataset metadata). So answering the question we're not deleting because that would yield incorrect maintenance results (we would stop deleting blocks after the first batch and leave all the other blocks as garbage that will possibly never be deleted).
There was a problem hiding this comment.
answering the question we're not deleting because that would yield incorrect maintenance results (we would stop deleting blocks after the first batch and leave all the other blocks as garbage that will possibly never be deleted)
I'm not following this, can you elaborate?
There was a problem hiding this comment.
Datasets are not deleted at once, but in increments of size batchSize. Because the dataset may actually be a lot larger than the batch size, we are forced to store a deletion "cursor" (the checkpoint) so that the maintainer picks up from where it left off during the next maintenance cycle. For instance, a dataset with
I think this is perhaps more complicated than it needs to be. We should talk about whether or not maintaining fixed batch sizes really make sense, cause I think being able to kill a dataset at once would simplify things. I'm also advocating limiting concurrency by locking datasets that are undergoing garbage collection so that any operation on the dataset gets forcefully reordered wrt ongoing deletion.
There was a problem hiding this comment.
OK looks like I haven't fully understood why we need checkpoints - the code tries to delete the entire dataset, updating the checkpoint at every batch. There is no actual interruption unless that's coming from outside, so not sure why this is needed.
There was a problem hiding this comment.
Checkpoints are optimization for storing cursor in case of some interruption like node shutdown. We could resume then from where we left (which may be useful for very large datasets). And yes, we try to delete all dataset blocks one after another.
codex/stores/maintenance.nim
Outdated
| await self.metaDs.modify(key, | ||
| proc (maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | ||
| if currDatasetMd =? maybeCurrDatasetMd: | ||
| if currDatasetMd.expiry != datasetMd.expiry or currDatasetMd.manifestsCids != datasetMd.manifestsCids: | ||
| raise newException(CatchableError, "Change in expiry detected, interrupting maintenance for dataset with treeCid " & $treeCid) | ||
|
|
||
| if currDatasetMd.checkpoint.progress > datasetMd.checkpoint.progress: | ||
| raise newException(CatchableError, "Progress should be increasing only, treeCid " & $treeCid) | ||
|
|
||
| if currDatasetMd.leavesCount <= datasetMd.checkpoint.progress: | ||
| DatasetMetadata.none | ||
| else: | ||
| datasetMd.some | ||
| else: | ||
| raise newException(CatchableError, "Metadata for dataset with treeCid " & $treeCid & " not found") |
There was a problem hiding this comment.
This is more readable imo:
| await self.metaDs.modify(key, | |
| proc (maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | |
| if currDatasetMd =? maybeCurrDatasetMd: | |
| if currDatasetMd.expiry != datasetMd.expiry or currDatasetMd.manifestsCids != datasetMd.manifestsCids: | |
| raise newException(CatchableError, "Change in expiry detected, interrupting maintenance for dataset with treeCid " & $treeCid) | |
| if currDatasetMd.checkpoint.progress > datasetMd.checkpoint.progress: | |
| raise newException(CatchableError, "Progress should be increasing only, treeCid " & $treeCid) | |
| if currDatasetMd.leavesCount <= datasetMd.checkpoint.progress: | |
| DatasetMetadata.none | |
| else: | |
| datasetMd.some | |
| else: | |
| raise newException(CatchableError, "Metadata for dataset with treeCid " & $treeCid & " not found") | |
| proc modifyData(maybeCurrDatasetMd: ?DatasetMetadata): Future[?DatasetMetadata] {.async.} = | |
| without currDatasetMd =? maybeCurrDatasetMd: | |
| raise newException(CatchableError, "Metadata for dataset with treeCid " & $treeCid & " not found") | |
| if currDatasetMd.expiry != datasetMd.expiry or currDatasetMd.manifestsCids != datasetMd.manifestsCids: | |
| raise newException(CatchableError, "Change in expiry detected, interrupting maintenance for dataset with treeCid " & $treeCid) | |
| if currDatasetMd.checkpoint.progress > datasetMd.checkpoint.progress: | |
| raise newException(CatchableError, "Progress should be increasing only, treeCid " & $treeCid) | |
| if currDatasetMd.leavesCount <= datasetMd.checkpoint.progress: | |
| DatasetMetadata.none | |
| else: | |
| datasetMd.some | |
| await self.metaDs.modify(key, modifyData) |
However, I also have a few comments:
- Returning DatasetMetadata.none seems like an odd way to indicate that the
metadata should be deleted. Maybe it might be better to handle the delete
logic later on when cleaning up? - Why are we raising an exception for metadata not found here? It seems like it
would be better placed fornim-datastoreto handle that, and this
predicate/callback would not be called because a failure would have been
returned further up the call stack. - This callback is
try/excepted up the callstack and turned into a Result
which eventually becomes the return value ofmodify. If we were to change
the signature of this callback to return a Result instead of raising
exceptions, then we know that returningfailure(err)in the callback will
become the returned value ofmodifyand hence would be a lot easier to swallow
as a reader. - Since these Results are ultimately bubbled up to
superviseDatasetDeletion,
it's a good idea to type them properly so that they can be inspected and
different failures can have differnt outcomes. For example, the "change in
expiry" that is being checked sounds like it would be a nasty bug of unknown
origin, so you may want to add a metric to it so you can monitor the
occurences better.
There was a problem hiding this comment.
- Keep in mind that it has to be done in a concurrent safe way, otherwise some anomalies will occur in the datastore, like deleting still used data or not deleting expired data.
modifyis the only way to perform concurrent safe updates to the records and it requires to returnnoneif delete is an intended result of the operation. - An exception is raised because it would be an error situation when we're trying to record a checkpoint when there's no
DatasetMetadatathat's related to the giventreeCid. If you would like to see API formodifychanged, please create an issue or maybe a PR in nim-datastore that would explain in detail how such API would look like. - This PR uses existing API in
nim-datastore. If you would like this API to be changed please raise such issue with detailed explanation in appropriate repo. - No, change in
expirywould not mean a nasty bug. It would mean that there was an update to the dataset metadata, that could be a result of for example reuploading a dataset just after it expired and maintenance already started but hasn't finished yet.
There was a problem hiding this comment.
Yeah... I'm seriously wondering if we shouldn't limit concurrency so we can make this easier to reason about. But let's talk about it.
There was a problem hiding this comment.
I believe @dryajov's comment address points 1-3. Regarding point 4, I think you
missed the point. The comment was meant to provide reasoning about why typing
exceptions is important. But as with the change that Dmitriy suggested, a Result
would be returned and exceptions would not need to be raised here.
There was a problem hiding this comment.
Once this API proposed by @dryajov will be available in nim-datastore I will use it. Even though I think it's pretty much the same as the current one with additional unnecessary complications to it.
23e8122 to
0ab4b1c
Compare
|
@emizzle please add a comment here whenever you will finish reviewing this PR. |
There was a problem hiding this comment.
Overall, I think this looks sound, but I can see that several aspects of the code, (e.g. error handling and overall style) have caused some controversy.
I think there are several reasons for this:
- the inherent style of the read-modify-write pattern, like the complex return state and the use of closures, which can be hard to read and reason about
- the way we're doing error handling, and overall handling return values
Seeing this in parcatice, I do see the issues with the design of modifyGet. For example, relying on optional to signal what operation to execute (update, delete or keep), limits error handling to only Exceptions. This isn't inherently wrong per se, but it makes the code harder to reason about and less consistent.
One way to address this would be to change modifyGet to be a bit more user friendly. For example by introducing a special return type structure that captures the semantics of the operations more closely, instead of relying on Option, which then opens the possibility of using Result to communicate error states more consistently.
Here is some pseudocode to better illustrate the idea and mull over the different possibilities.
type
Operations = enum
Keep,
Update,
Delete
OpResult[T] = object
case op: Operations
of Keep: discard
of Update:
val: T
of Delete: discard
Function*[T, U] = proc(value: T): U {.raises: [CatchableError], gcsafe, closure.}
ModifyGet* = Function[?!OpResult[seq[bytes]]]]
method modifyGet*(self: Datastore, key: Key, fn: ModifyGet): Future[?!seq[byte]] {.base, locks: "unknown".} =
let maybeCurrentData = ... # get current data val
whitout op =? fn(maybeCurrentData), err:
return failure(...)
case op.op:
of Keep: ... # keep val
of Update: doUpdate(key, op.val) # update entry
of Delete: doDelete(key) # delete entry
success(...)
This makes it easier to reason about the implementation of the update fn, and allows for more consistent error handling style.
Given that this is a limitation of the underlying datastore, and not so much this code, I think we can move this PR forward, provided we address some of the other comments left by the other reviewers and myself. However, I would strongly suggest that we do think about improving modifyGet further and make the required changes asap.
codex/stores/maintenance.nim
Outdated
| if err =? (await self.recordCheckpoint(treeCid, datasetMd)).errorOption: | ||
| return failure(err) |
There was a problem hiding this comment.
answering the question we're not deleting because that would yield incorrect maintenance results (we would stop deleting blocks after the first batch and leave all the other blocks as garbage that will possibly never be deleted)
I'm not following this, can you elaborate?
codex/stores/maintenance.nim
Outdated
| without queryKey =? createDatasetMetadataQueryKey(), err: | ||
| return failure(err) | ||
|
|
||
| without queryIter =? await query[DatasetMetadata](self.metaDs, Query.init(queryKey)), err: |
There was a problem hiding this comment.
How big is this query going to be, we should be careful and perhaps use pagination if this gets too large.
There was a problem hiding this comment.
It depends on how many datasets are we storing. I would say that number probably will not ever exceed 1k in practice. But if you want I can add pagination.
|
|
||
| self.timer.start(onTimer, self.interval) | ||
| if self.interval.seconds > 0: | ||
| self.timer.start(onTimer, self.interval) |
There was a problem hiding this comment.
I would avoid using the timer altogether and just use a regular async for here?
There was a problem hiding this comment.
What you mean is this?
while (self.notStopped):
doStuff()
await asyncSleep(self.delay)There was a problem hiding this comment.
For now I left the timer since I didn't get the confirmation if what I proposed above is what you mean.
|
One more comment, I think moving the closures to their own named functions does improve readability somewhat, but it is really marginal, so I'm either way on this and consider it more a matter of style that anything more substantive. |
There was a problem hiding this comment.
I agree with the suggested change from @dryajov above, and believe this would clear up quite a lot of the readability and reasoning difficulties I experienced as an outside reader.
I want to reiterate my review motivation throughout this PR, so there is no ambiguity. When I write code, I'm very much motivated by these two things:
- It is readable
- It is easy to reason about
The main thing is that as a writer, consideration of these points means you are always trying to see what you've written from a reader's perspective. Why is this important? Because if code is not readable and is not easy to reason about, it becomes, to a degree, technical debt.
I understand that these two goals are highly subjective, but what I find valuable as a writer is when a reader provides feedback about those points. I consider myself an "outside reader" of this code since I'm less involved in the client on a daily basis, and all of the suggestions I've made are simply my subjective opinion on how to potentially improve those two things. It doesn't mean I think I'm "right" about any of it, because if there's anything I've learned, being right about everything is only good for digging ditches 😂
A positive takeaway from this PR is that we all are very passionate about our quality of work and Codex itself and that is something we should all be happy about ❤️
gmega
left a comment
There was a problem hiding this comment.
My main concerns are with all the concurrency issues I think we may still face with the absence of dataset-level locking, as well as a number of other places where I see we're open to more complexity due to support of transactional updates within our datastore itself.
codex/stores/maintenance.nim
Outdated
| DefaultNumberOfBlocksToMaintainPerInterval* = 1000 | ||
| DefaultDefaultExpiry* = 24.hours | ||
| DefaultMaintenanceInterval* = 5.minutes | ||
| DefaultBatchSize* = 1000 |
There was a problem hiding this comment.
Hm... as I am reading through this, I am confused by the meaning of batch size in this context. It used to be the number of blocks I'm willing to go through at every garbage collection cycle, but now that expiration is per dataset I can't understand what this means anymore (am I willing to look into 1000 datasets per cycle? That looks too much. Is it still the total number of blocks? Then it's weird, cause this means a dataset can be partially purged if it has more blocks than that). I'll eventually figure it out by reading code, but this is no longer self-explanatory.
There was a problem hiding this comment.
BatchSize means number of blocks deleted before recording a checkpoint. If you want I can either add docs here or remove this param altogether with removing checkpoints and replace it using bisect.
There was a problem hiding this comment.
Renaming to CheckpointLength and adding docs.
codex/stores/maintenance.nim
Outdated
| if err =? (await self.recordCheckpoint(treeCid, datasetMd)).errorOption: | ||
| return failure(err) |
There was a problem hiding this comment.
Datasets are not deleted at once, but in increments of size batchSize. Because the dataset may actually be a lot larger than the batch size, we are forced to store a deletion "cursor" (the checkpoint) so that the maintainer picks up from where it left off during the next maintenance cycle. For instance, a dataset with
I think this is perhaps more complicated than it needs to be. We should talk about whether or not maintaining fixed batch sizes really make sense, cause I think being able to kill a dataset at once would simplify things. I'm also advocating limiting concurrency by locking datasets that are undergoing garbage collection so that any operation on the dataset gets forcefully reordered wrt ongoing deletion.
codex/stores/maintenance.nim
Outdated
| if err =? (await self.recordCheckpoint(treeCid, datasetMd)).errorOption: | ||
| return failure(err) |
There was a problem hiding this comment.
OK looks like I haven't fully understood why we need checkpoints - the code tries to delete the entire dataset, updating the checkpoint at every batch. There is no actual interruption unless that's coming from outside, so not sure why this is needed.
codex/stores/maintenance.nim
Outdated
| if numberReceived < self.numberOfBlocksPerInterval: | ||
| self.offset = 0 | ||
| if (datasetMd.expiry < self.clock.now) and | ||
| (datasetMd.checkpoint.timestamp + self.retryDelay.seconds < self.clock.now): |
There was a problem hiding this comment.
Yeah OK I really need to understand this better:
- when do we expect deletes to fail;
- why is retrying a strategy (assumes a transient condition).
There was a problem hiding this comment.
- There can be multiple reasons for that. I.e. fail to open db file due to too many file descriptors open.
- Because pt. 1. can happen or node can shutdown mid point dataset deletion.
|
|
||
| proc tryDeleteBlock*(self: RepoStore, cid: Cid, expiryLimit = SecondsSince1970.low): Future[?!DeleteResult] {.async.} = | ||
| proc tryDeleteBlock*(self: RepoStore, cid: Cid): Future[?!DeleteResult] {.async.} = | ||
| if cid.isEmpty: |
There was a problem hiding this comment.
The fact that metadata and block state are not updated transactionally makes me a bit anxious. I can see for instance that there is room for metadata to be left behind after a block deletion.
There was a problem hiding this comment.
Not sure how to address this.
The remainder of the points I raised can be handled with a separate PR in nim-datastore
benbierens
left a comment
There was a problem hiding this comment.
sha-0ab4b1c branch passes basic dist-tests.
8e1699b to
950c729
Compare
950c729 to
e9972d1
Compare
|
We're rewriting the storage layer so that expiring datasets will become much more natural; we won't be needing this anymore. |
No description provided.