-
Notifications
You must be signed in to change notification settings - Fork 19
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
Rework AsyncIter #811
Rework AsyncIter #811
Conversation
4db0594
to
bf84aa2
Compare
fc13503
to
23811a2
Compare
fc555d8
to
9bbe0f6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice improvements all around.
): Future[?!AsyncIter[?!QueryResponse[T]]] {.async.} = | ||
## Converts `QueryIter[T]` to `AsyncIter[?!QueryResponse[T]]` and automatically | ||
## runs dispose whenever `QueryIter` finishes or whenever an error occurs (only | ||
## if the flag finishOnErr is set to true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is another case in which a queryIter must be disposed. This is when an iteration is stopped early, before all items have been yielded. LevelDB showed that we were leaking unfinished iters. Sqlite would clean them up automatically on db.close. But LevelDB will throw instead.
I'm not sure this impacts your changes. But please make sure that the QueryIter is disposed correctly, even when the async iter does not error and does not run to completion.
See the TODO here reservations.nim
in proc findAvailability
:
nim-codex/codex/sales/reservations.nim
Line 629 in 3246c43
# TODO: As soon as we're on ARC-ORC, we can use destructors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this comment. I realise that there's a flow like that, but I didn't realise we can address this flow when we have ARC. Current use cases are not finishing iteration prematurely, so either all items have been traversed or an error occurred - both cases are handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it looks in general hard to detect an abandoned iterator unless you can tap into GC hooks. Maybe have an explicit destruct
or close operation for query iterators which clients should call if they decide not consume its results anymore? The other option I can think of is a timeout, but that's even trickier I think...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this. The query iterator has a dispose
you're supposed to call when you're done with the thing. It's done automatically when you complete the iterator and so no one calls dispose. Which is why it started causing problems in this case I linked when levelDB was rolled out. It made the problem visible.
Ideally, when the iterator object is cleaned up, we dispose the iterator handle. The current GC doesn't give us this hook, hence the TODO.
tds: TypedDatastore | ||
|
||
setupAll: | ||
tds = TypedDatastore.init(SQLiteDatastore.new(Memory).tryGet()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be useful to use the LevelDB one, just because it is more picky about disposing iterators. It'll help find issues.
There is no in-memory LevelDB datastore. There's a helper called TempLevelDb
that is used in tests for this. Examples everywhere! :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer such tests to be done in memory only. Maybe there's some in-memory filesystem that we can use for LevelDb?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked into this. I don't mind either way. It's just that I noticed using the levelDB for real changes the timing behavior, which has already in the past revealed issues we didn't see. So you can keep the test Sqlite if you want. I'd still suggest you at least run it a few times with the levelDB thing instead, just to see nothing mysterious shows up.
teardownAll: | ||
(await tds.close()).tryGet | ||
|
||
test "Should auto-dispose when QueryIter finishes": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading this right, this test runs the iter to completion. It'd be really nice to have a test that does not. So it iterates only the first few items of a longer list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what such test would suppose to check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe @benbierens wants to guarantee that the query iterator is disposed even if it's not run to completion (which, I think, currently it won't be).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, not sure if that's possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes!
You see, if you use the leveldb helper, then in the setup you create the DB and in teardown you dispose it. If your code does not correctly release the iterators, then your teardown will throw. This way you know there's a problem. (Sqlite doesn't do this, it quietly ignores undisposed iterators.)
So then simply having a test that partially iterates a list would reveal the problem if it's there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So then simply having a test that partially iterates a list would reveal the problem if it's there.
I mean we know that the iterator will not be disposed in this case. If I would add such test it would always fail on the teardown. Also such test would be impossible to fix currently according to the TODO
mentioned in this comment.
We could try to assert that iter isn't disposed in such scenario, but that's even worse, because we would specify (tests are specs) that the expected behavior is not disposing an iterator. Therefore I just prefer to leave it unspecified.
If I'm missing something please provide me an example how such test should look like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds to me like using the query-iter-helper will guarantee we leak the iterator whenever a query is not run to completion. This can be solved by the other GC in the future. But I suppose in the meantime, we need a way to manually dispose the iter when the user of the iter knows that it's not going to complete.
Either asyncIter has to support manually disposing (for now), or we shouldn't use it instead of queryIter. :| This sucks, but crashes due to leaks or other memory issues are a nightmare to even detect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please elaborate on how using this module will guarantee that "we leak the iterator", maybe an example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am puzzled by some things, but maybe because I'm lacking context. I won't block the approval of this PR though as you have enough people asking for things there already. :-)
codex/erasure/erasure.nim
Outdated
@@ -120,7 +120,7 @@ proc getPendingBlocks( | |||
CatchableError, | |||
"Future for block id not found, tree cid: " & $manifest.treeCid & ", index: " & $index) | |||
|
|||
Iter.new(genNext, isFinished) | |||
newAsyncIter[(?!bt.Block, int)](genNext, isFinished) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... this is sort of different from our style guide. Any reason to have it done like that instead of AsyncIter.new?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new style will not work well with generics to my knowledge. This call is not going to work: AsyncIter.new[T](...)
hence the old style used here and to keep it consistent it's used in both Iter
and AsyncIter
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really? This seems to compile fine here:
type MyGeneric[U, V] = ref object of RootObj
u: U
v: V
proc new*[U, V](t: type MyGeneric[U, V], u: U, v: V): MyGeneric[U, V] =
MyGeneric[U, V](u: u, v: v)
var a = MyGeneric[int, string].new(1, "hello")
echo a.u, " ", a.v
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I haven't tried to use
AsyncIter[T].new(...)
I just tried
AsyncIter.new[T](...)
and that lead to compilation errors. I will the first notation and see if it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used AsyncIter[T].new(...)
and it works! Thanks for the suggestion @gmega 👍
): Future[?!AsyncIter[?!QueryResponse[T]]] {.async.} = | ||
## Converts `QueryIter[T]` to `AsyncIter[?!QueryResponse[T]]` and automatically | ||
## runs dispose whenever `QueryIter` finishes or whenever an error occurs (only | ||
## if the flag finishOnErr is set to true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it looks in general hard to detect an abandoned iterator unless you can tap into GC hooks. Maybe have an explicit destruct
or close operation for query iterators which clients should call if they decide not consume its results anymore? The other option I can think of is a timeout, but that's even trickier I think...
isFinished = () => iter.finished | ||
) | ||
|
||
proc mapFilter*[T, U](iter: Iter[T], mapPredicate: Function[T, Option[U]]): Iter[U] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we use result objects and handle errors in mapPredicate/iter.next in the async version, but here we don't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well there's some handling already provided by newIter
, so the iter will finish normally. However it's not perfect (last item before error will get lost I think). It's simply a bit more difficult in non-async version, so I decided to not implement it. I will add it though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Last item before error should no longer get lost.
teardownAll: | ||
(await tds.close()).tryGet | ||
|
||
test "Should auto-dispose when QueryIter finishes": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe @benbierens wants to guarantee that the query iterator is disposed even if it's not run to completion (which, I think, currently it won't be).
check: | ||
items == @[1, 3] | ||
|
||
test "Should leave only odd items using `mapFilter`": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a test path for the error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In line 79 there's a test that checks if iter finishes on err.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file only goes until line 77 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably you look on the outdated version of this file. I added tests for checking errors after @benbierens comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing major on my end so provided you address the rest LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally LGTM, I would only like to point out the "catch" of CancelledError
being inherited from CatchableError
which should be then reraised and not swallowed. For more see Mark's post about it: https://discord.com/channels/895609329053474826/1175098439504244767/1242444718231523338
I think I found one potential place where this should be handled (see the other comment), but generally please have a look on your changes from this POV if maybe there are some other non-handled cases.
futU.complete(u) | ||
nextFutU = some(futU) | ||
break | ||
except CatchableError as err: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not completely sure, but potentially you should check for CancelledError
and re-raise it here: https://discord.com/channels/895609329053474826/1175098439504244767/1242444718231523338
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very valueable comment, thanks for pointing it out. I will fix it and add tests to make sure cancellation gets properly propagated.
proc fromSlice*[U, V: Ordinal](_: type Iter, slice: HSlice[U, V]): Iter[U] = | ||
## Creates new iterator from slice | ||
## | ||
proc new*[U, V: Ordinal](_: type AsyncIter[U], slice: HSlice[U, V]): AsyncIter[U] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear anymore what any of this constructors do by themself. Lets add some comments to document it please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, except for the documentation comment. But we can address in a separate PR.
AsyncIter[T]
andIter[T]
asynciter.nim
anditer.nim
QueryIter
toAsyncIter