All of this sits behind a "normal" class that is meant to be used from the main GUI thread. The reason for all this threading is to allow plugins to be written in a way that does not require them to be asynchronous (many types of query matches use synchronous API) while ensuring that matching is done in a parallel fashion. This allows the GUI to remain fluid while keeping and allows matches to be returned as quickly as possible. It also means using Sprinter from an application is dead simple: create a RunnerManager, add it to a view (it is a model!) and send it queries.
This design makes the internal implementation more intricate. I hesitate to say "complex" because that may raise images of code that is difficult to wrap your head around, which isn't the case with Sprinter in my opinion: it's all rather straight-forward code, it just has more "moving parts" than if it was a purely synchronous solution.
Those moving parts do lead to some moments of head scratching, though. I sat down to write some documentation for the library and realized that a couple of the signals (executionStarted and executionFinished) from RunnerManager did not have signatures that were symmetrical with other related methods. So I set about fixing that by having the signals include the index of the match that was executed. This was simple enough until I realized that the following could happen quite easily:
- The user requests a match to be executed (e.g. open a URL or whatever the relevant action might be)
- The application calls executeMatch(index); this is very convenient as RunnerManager is a model so all matches have indexes
- executionStarted(index) is emitted and the match is executed asynchronously
- In the meantime, more matches arrive from the threads which are still processing the query
- These matches are scheduled for synchronization
- The match has been executed and executionFinished(index, success) is emitted
My first thought was, "Crap, this is going to add complexity that I don't want ..." but then the magical fairy of pragmatic laziness settled upon my shoulder and whispered into my ear, "Dude, why do you have to update the model if a match is being executed? In nearly all cases, that means the user is done anyways so just leave the data in the model alone until the execution is complete." She faded away, returning back to wherever magical fairies spend their time when they aren't helping busy coders, and a smile appeared on my face. The answer was really so simple that the implementation would be trivial.
Sprinter now pauses synchronization of matches from the threadpool into the model (which lives in the GUI thread) while a match is being executed. As long as the GUIs doesn't hold on to stale indexes (which it shouldn't be doing anyways due to how Qt models work) this should never fail due to race conditions from the threading. (I do hope I haven't just invoked Murphy's Law there.)
The design of Sprinter then delivered a few free gifts: matching will continue on in the background while the execution is occurring, and when execution is complete any matches that appeared during that time will automatically synchronize into the model which will cause the GUI to update with the new stuff. The application can prevent that from happening by simply calling endQuerySession() when executionFinished is emitted, as the finished signal is sent before synchronization starts. The user can now execute multiple matches, all at once or one after another, and everything works as expected without surprises. Boringness achieved.
There are caveats-in-theory: matches that get updated by a runner won't show that updated data until execution is finished. In practice, I don't think this will matter at all, since all of the applications I've looked at which use Plasma::RunnerManager (or some of the other similar frameworks out there) don't really care post-execution about the match data. For GUIs that might exist in future and behave differently here: match execution should be fast (in human timescales) which is what matters in relation to the GUI showing fresh data. It's a small price to pay for drop-dead-simple guaranteed consistency.
Always listen to the magical fairy of pragmatic laziness.