-
Notifications
You must be signed in to change notification settings - Fork 27
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
Add async query support #30
Conversation
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 like the small & fast PR very very much! 🎩
Left a few comments, but I didn't dive too deep / compiled locally (for now at least 😄 ).
From the linked docs, I think the actual signature should be something like
let result: impl Stream<Item=HashMap<String, Variant>> = con.async_raw_query("SELECT Name FROM Win32_OperatingSystem")?;
Since WMI will indicate
to the sink multiple times as more items become available.
Guessing even more, I think WMI will call this function from another thread, so be sure to wrap everything in a real Mutex
(and not the "just" an async
one) or something equivalent (for example, using a queue between the sink and the actual Stream
) [and document that either way]. I don't think that ComPtr
handles that but again I didn't check it myself.
Hello @ohadravid and Merry Christmas ! Is it ok to trigger your azure pipeline each time I push ? Otherwise I could disabled it temporarily for this branch ? I have to take into account several of your comments, thanks a lot for reviewing quickly ! EDIT: For the public API, I think I won't pub the exec_async_query_native_wrapper. |
Is should probably be public, since it's still useful as an API. I think the pipeline won't mind. Merry Christmas! 🎄 |
We had the same idea ! I was looking at implementing an async mspc too :D let (tx, rx) = mpsc::channel();
let p_sink: ComPtr<IWbemObjectSink> = QuerySink::new(tx); But it would fail as tx isn't Sync. I'll go with async_channel it's perfect for me. Thanks a lot ! |
Hello, The async mpsc seems to work, it was a very good idea. For the moment, I send a Vec of results for each Indicate call, the Future will have to consolidate them once the channel is closed. I can't foresee any implementation hurdle right now after this quick and dirty try |
src/query_sink.rs
Outdated
|
||
let lObjectCount = lObjectCount as usize; | ||
let tx = self.sender.clone(); | ||
let mut result = Vec::<IWbemClassWrapper>::with_capacity(lObjectCount); |
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.
Any reason not to use a Sender<IWbemClassWrapper>
directly, and send each object directly to the channel?
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 see 2 reasons by now :
Sink implementations should process the event notification within 100 MSEC because the WMI thread that delivers the event notification cannot do other work until the sink object has completed processing. If the notification requires a large amount of processing, the sink can use an internal queue for another thread to handle the processing.
(https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemobjectsink-indicate) => maybe It would take too much time to call the transmitter on each element, but I could be wrong.
The other reason would be that I don't see yet the need to iterate on the receiver side on each element.
It's not set in stone yet
BTW, the way I understand the previous quote, I think the Indicate function won't be called from 2 different threads, but I have to confirm this. I don't think it will impact my implementation anyway, but that could be a good news
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 think that send
ing to the channel should be almost immediate as it's not blocking (or at most on the order of a millisecond).
The other reason would be that I don't see yet the need to iterate on the receiver side on each element.
If I understand correctly, indicate can be called multiple times, so if you use the vector the other side will receive an entire vector each time, and will need to iterate on it and return items from it, before moving on to the next vector. It seem simpler to have a "flat" channel so both side just send/recv a single object at a time.
|
||
let mut arr = vec![ptr, ptr2]; | ||
|
||
unsafe {p_sink.Indicate(arr.len() as i32, arr.as_mut_ptr());} |
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.
Cool test! I like this very much :)
I like it, but thinking about this some more, a simpler & more "real" test would probably be executing a query which return a few items, and iterating over the resulting stream.
For example, here you don't simulate the "real" way Indicate
passes/not passes ownership 🤔
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 completed the test, it should be now splitted into several smaller tests, but I checked the refcount by releasing the first pointers, so that it mimics how the Indicate call will be done.
EDIT: now there should be too much Release at the end of the tests, I'm trying to find what happens in this case...
I'll quickly fix the tests first
EDIT2: quick fix available
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 didn't resolve this conversation as I did not take exactly what you suggested into account.
Here I make some unit testing on the indicate
and set_status
methods. The "real" test is made in async_query.rs
.
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 think this is looking really good!
Perhaps you can cleanup & handle the needed errors, and return a Result<impl Stream<Item=Result<IWbemClassWrapper, WMIError>>>
by returning the receiver
and we can merge this already!
Hello! I wish you an happy new year, all the best for 2021 ;) I tried a small experiment with the I'm launching the tests with the following command line : As it is a different implementation, I made another branch. But at least I could craft a test for the
If I understand correctly, the Release method provided by com_impl is called too many time, so WMI releases the QuerySink and decrement the refcount below zero, which causes this stacktrace. So I'm a bit stuck there at the moment... I don't understand why QuerySink is realeased more than once by WMI. EDIT: I guess those are the worst errors, the one that are not consistent between 2 runs...:
My wild guess would be that the Release is sometime called before the AddRef occured, i don't know how it can be possible...
|
Hello there, I tried a similar implementation as the one in the other branch but with async_channel. Test is still failing with the refcount problem... I thought maybe query_sink is released by rust because there is no real way for the compiler to know that // Execute the given query in async way, returns result in a Sink.
#[cfg(feature = "async-query")]
pub async fn exec_async_query_native_wrapper(
&self,
query: impl AsRef<str>,
) -> Result<Vec<Result<IWbemClassWrapper, WMIError>>, WMIError> {
let query_language = BStr::from_str("WQL")?;
let query = BStr::from_str(query.as_ref())?;
let (tx, rx) = async_channel::unbounded();
let p_sink: ComPtr<IWbemObjectSink> = QuerySink::new(tx);
unsafe {
check_hres((*self.svc()).ExecQueryAsync(
query_language.as_bstr(),
query.as_bstr(),
WBEM_FLAG_BIDIRECTIONAL as i32,
ptr::null_mut(),
p_sink.as_raw(),
))?;
}
// TODO: transform to Result<Vec<IWbemClassWrapper>, WMIError>
// TODO: try not to use collect as it adds dependency to futures::stream::StreamExt
Ok(rx.collect::<Vec<_>>().await) // <= query_sink must be released only after await finishes
} So I tried the following version but it's failing in the exact same way... // Execute the given query in async way, returns result in a Sink.
#[cfg(feature = "async-query")]
pub async fn exec_async_query_native_wrapper(
&self,
query: impl AsRef<str>,
) -> Result<Vec<Result<IWbemClassWrapper, WMIError>>, WMIError> {
let query_language = BStr::from_str("WQL")?;
let query = BStr::from_str(query.as_ref())?;
let (tx, rx) = async_channel::unbounded();
let p_sink: ComPtr<IWbemObjectSink> = QuerySink::new(tx);
unsafe {
p_sink.AddRef();
check_hres((*self.svc()).ExecQueryAsync(
query_language.as_bstr(),
query.as_bstr(),
WBEM_FLAG_BIDIRECTIONAL as i32,
ptr::null_mut(),
p_sink.as_raw(),
))?;
}
// TODO: transform to Result<Vec<IWbemClassWrapper>, WMIError>
// TODO: try not to use collect as it adds dependency to futures::stream::StreamExt
let result = rx.collect::<Vec<_>>().await;
unsafe { p_sink.Release(); } // now p_sink shouldn't be released before the end of await
Ok(result)
} I don't know if you can spot something obviously wrong, I hope you can :) EDIT: woops broke the pipeline ! Quick precision, I launch my tests with |
Hello, Quick update on the subject: just to be able to go forward, I "hacked" the RefCount by calling Otherwise I made some refactoring, to isolate all the code related to the feature in a separate async_query file, it's much nicer for the feature flipping! Also changed the signature of pub fn exec_async_query_native_wrapper(
&self,
query: impl AsRef<str>,
) -> Result<impl Stream<Item=Result<IWbemClassWrapper, WMIError>>, WMIError> { As you suggested, it's now returning a Stream. Regards, |
Hello, There's 2 things to "fix":
Regards, |
Hi @apennamen, I'll try to review this over the weekend 😁 |
} | ||
|
||
let lObjectCount = lObjectCount as usize; | ||
let tx = self.sender.clone(); |
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 the clone
?
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 unnecessary, I wanted to be sure that each thread gets its own Sender
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.
But the channel is Send+Sync, so even for different threads, it should be ok to use the same sender (I think)
return E_FAIL; | ||
} | ||
} else { | ||
if let Err(e) = tx.try_send(Err(WMIError::NullPointerResult)) { |
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 nice :)
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.
@apennamen awesome, thank you for working on this :)
I'll merge it now and release a version soon.
Cheers!
} | ||
|
||
let lObjectCount = lObjectCount as usize; | ||
let tx = self.sender.clone(); |
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.
But the channel is Send+Sync, so even for different threads, it should be ok to use the same sender (I think)
General Information
WMI provides an ExecQueryAsync method. This PR aims at providing an implementation and expose an API looking like :
Motivation is described in #13.
WIP
async-query
indicate
andset_status
in QuerySink based on this example. Maybe find other inspirations.async_raw_query
pub fn async_raw_query
in query.rs