Hi all,
Provide-until is already full of surprises. Here goes a crazy idea for dealing with a problem I encountered while using it.
Problem
Suppose you have a process that implements a chat room. It works like this: first you open the chat room; then, operations getHistory (for reading the content of the chat room) and postMessage (for posting a new message) are provided until operation closeChatRoom is called. We can elegantly encode this with the provide-until statement:
createChatRoom( room );
history = "";
provide
[ getHistory()( history ) { nullProcess } ]
[ postMessage( msg )() { history += msg + "\n" } ]
until
[ closeChatRoom() ]
Nice, but inefficient: this is a typical readers/writers problem, but provide-until is currently always sequential. We should instead allow getHistory to be invoked in parallel, as it doesn't change our shared state. Differently, postMessage should indeed be sequential (no other threads should be changing the value of history while it is operating).
Solution 1 - Concurrent provide-until
We could introduce the option of telling provide-until which operations should be provided concurrently and which sequentially, for example:
provide!
[ getHistory()( history ) { nullProcess } ]
provide*
[ postMessage( msg )() { history += msg + "\n" } ]
until
[ closeChatRoom() ]
The semantics of this would be:
- all provide! operations are provided in parallel;
- a provide* operation waits for all current invocations of provide! operations to terminate before executing, and likewise all other operations (including the provide!) cannot start until this operation invocation terminates;
- until works as a provide* operation, but also terminates the statement.
Solution 2 - Programmable concurrency for provide-until
The solution above works for typical readers/writers but there may be scenarios where it doesn't suffice. In such cases, the programmer may want to specify a custom policy for telling Jolie when an operation should become available, maybe using internal links. Assume that all provide operations can go in parallel, then here is an example of how to realise the scenario (forgive the long syntax):
/* This block is called R,
and is active only when no operation calls are being processed in block W */
provide[R requires W]
[ getHistory()( history ) { nullProcess } ]
/* This block is called W,
and is active only when no operation calls are being processed in blocks R and W */
provide[W requires R,W] [ postMessage( msg )() { history += msg + "\n" } ]
// This blocks is active only when no other operation call is in progress in the other blocks
until
[ closeChatRoom() ]
Solution 3 - Asynchronous Parallel
Another solution could be to use recursion. The current provide-until block can be simulated with recursion, for example the following
provide
[ getHistory()( history ) { nullProcess } ]
[ postMessage( msg )() { history += msg + "\n" } ]
until
[ closeChatRoom() ]
can be implemented as:
define X {
[ getHistory()( history ) { nullProcess } ] { X }
[ postMessage( msg )() { history += msg + "\n" } ] { X }
[ closeChatRoom() ]
}
This, however, fails if we want concurrency for getHistory:
define X {
[ getHistory()( history ) { X | nullProcess } ]
[ postMessage( msg )() { history += msg + "\n" } ] { X }
[ closeChatRoom() ]
}
The reason is that X | nullProcess will make getHistory wait for the execution of X, which is not what we want. To get it right, we would need X to be able to proceed in parallel and not wait for it. Suppose to have this operator, || :
define X {
[ getHistory()( history ) { X || nullProcess } ]
[ postMessage( msg )() { history += msg + "\n" } ] { X }
[ closeChatRoom() ]
}
Now X is executed and getHistory returns immediately, without waiting for X to terminate.
But wait, now postMessage can be active before some getHistory call is still computing. So we would also need a magical operator for postMessage to wait for all current parallel threads handling getHistory to terminate:
define X {
[ getHistory()( history ) { X || nullProcess } ]
[ postMessage( msg )() { wait_for_all_getHistory(); history += msg + "\n" } ] { X }
[ closeChatRoom() ]
}
Conclusions
I hope you agree that provide-until can become more useful by powering it up a little. If not and you have an idea of how to do this with something we already have, I would be even happier because it would entail less development. ;-)
I showed some different potential solutions. Which to follow?
For now my preference is option 1 over 2 over 3. I dislike 3 as it is less declarative and simple, but we should always think about what we can do already before introducing something new, albeit convenient.
I find it really interesting that while provide-until right now (let's call it sequential provide-until) is just syntax sugar, it looks like that handling concurrency would make it a full-standing primitive in its own right.
A side note on execution modalities
It looks like having option 1 or 2 would effectively mix our three execution modalities (concurrent, sequential, and single) in a single primitive in a way that makes sense. I really like this, and maybe understanding this could pave the way for mixing execution modalities in the same service in a similar way, in the next-next-version.