Hi Tim, replies inline,
Timothy Ward wrote:
In my view Mockito mocks are frequently about record first / invoke later.
In my tests I often create a mock, run my test, and then validate that a
certain action has happened, e.g.
/* Setup */
MyListener listener = Mockito.mock(MyListener.class);
Manager manager = new Manager();
manager.addListener(listener);
/* End of setup */
manager.manage("foo");
/* Test assertions */
Mockito.verify(listener).startedManaging("foo");
I think we are misunderstanding each other's terminology. I'm assuming that
your example looks like this:
manager.manage("foo");
... listener.startedManaging("foo"); // #1
...
Mockito.verify(listener).startedManaging("foo"); // #2
The point I'm making is that the invocation of the target method happens at
#1, ie immediately when it is called. The second call to the startedManaging
method in #2 is not the invocation, even though it makes use of recorded
data from #1. The system changes state in #1, not in #2. That is why I'm not
labling this as record first / invoke later, but rather invoke(+record)
first / verify later.
So if we mirror Mockito's invoke first pattern:
listener.startedManaging("foo"); // perform+record invocation, system
changes state
Mockito.verify(listener).startedManaging("foo"); // use recording for
verification
with your invoke later pattern in Async:
listenerMediator.startedManaging("foo"); // record invocation
asyncService.call(); // use recording to perform invocation, system
changes state
then I hope you see what I mean.
1) Did you choose the record first / invoke later design because of
implementation considerations/compromises, or because you feel it is more
natural and intuitive for the user of the API?
Obviously there is a balance to be struck here. Primarily an API needs to
work well for a given set of common use cases, but it does also need to be
implementable in a moderately sane way. Creating a fire-and-forget call
against a service is relatively easy with a threadpool and a runnable, so
the particular target use case we are optimising for is the one where a
client wants to obtain a Promise for the asynchronous work that they're
starting. If you take this as a starting point then I think that the current
API feels pretty natural, although I understand that you may disagree.
Ok, so how would the Async API behave in the following exceptional
scenarios:
Method call without following Async.call:
asyncList.clear();
Async.call without preceeding method call:
Promise<Void> p = asyncService.call();
Multiple method calls before Async.call:
asyncList.clear();
asyncList.clear();
Promise<Void> p = asyncService.call();
or similar but with further complexity, assuming there is first an event
from System 1 and 10 minutes later an event from System 2:
class Main {
Async asyncService = ...
void start() {
a = new A(asyncService);
registerForEventsFromSystem1(a);
b = new B(asyncService);
registerForEventsFromSystem2(b);
}
}
class A {
...
void receiveEvent() {
List asyncList = asyncService.mediate(list);
asyncList.clear();
// programmer forgot asyncService.call()
}
}
class B {
...
void receiveEvent() {
List anotherAsyncList = asyncService.mediate(anotherList);
anotherAsyncList.clear();
Promise<Void> p = asyncService.call();
}
}
2) What mechanisms do you have in mind that implementations could use for
transferring the recorded calls to Async.call? I'm guessing ThreadLocal is
the main alternative?
Obviously ThreadLocal is an option, but the OSGi specifications try to avoid
dictating details at this level. I can see a model where an implementation
uses the value returned by the mediator to track state, although this does
get interesting with the zero-args call method. [assuming you meant void
return type]
I think this also gets interesting (=hard) with methods that return
instances of final classes? (can't use dynamically generated subclasses of
the return type to carry call state)
Is there a particular reason that you're interested in this detail?
Just trying to find the borders of the system, knowing what rules the API
must adher to not to make implementation impossible in the desired ways.
Even if the mediator were responsible for immediately starting the
asynchronous invocation it would still be necessary for the Async
implementation to track the call so that a Promise could be obtained for it
later. As I mention above, obtaining a Promise for the invocation is
probably the most important use case as far as we're concerned
Here we are in full agreement ;-)
Best regards
Mike
Timothy Ward wrote:
Hi Mike,
Your interpretation of the behaviour described in the RFC is correct,
including the roles of the mediator object and the async service. I'll
outline the design process that we went through, and hopefully that will
answer your question.
One of the core problems associated with making this sort of asynchronous
invocation is that you need to intercept a method call, immediately return a
value that isn't the real result, eventually provide the result through some
other means.
It turns out that these requirements are very similar to those of a test
mocking framework, for example Mockito. In Mockito you create a mock (or
mediated) object, and the invocations made upon it are recorded. Configuring
one of these mocks typically involves making the method call you wish to
mock, e.g.
Mockito.when(mock.doStuff()).
This pattern works very well for the mediators produced by the Async Service
too. The return type information from the mediator allows the generic type
of the Promise to be determined. Given that the pattern was a natural fit,
and the fact that mocking frameworks are widely used and understood, it was
logical to reuse it.
In addition to the parallels with mocking, there were several other aspects.
* The return value from a method call is rarely a Promise/Future,
which means that there must be some other mechanism to acquire the result.
If the mediator calls directly began the asynchronous work then it would be
easy to accidentally overwrite the Promise representing an earlier
execution. The current API flow allows Async implementations to verify the
API usage, and to throw exceptions if the mediator is used incorrectly.
* Mediated objects are not thread safe, and should not be shared
between bundles. The reason that they shouldn't be shared is because they
would allow other bundles to act using your bundle context. Requiring them
to be used with the Async service encourages safer coding patterns.
In summary the mediated object isn't actually a proxy (which is why we chose
the method name mediate, rather than proxy). The mediator simply records the
method calls made upon it, and has nothing to delegate to. The Async
implementation is the service providing the asynchronous behaviour, which is
why it is the one that gets called to begin the execution.
I hope that makes sense.
Regards,
Tim
On 14 Mar 2014, at 13:28, Mike Wilson <[email protected]> wrote:
Async RFC 206 (from March 03) at:
https://github.com/osgi/design/tree/master/rfcs/rfc0206
>From section 5.4 we learn how to start an asynchronous
invocation that has a return value:
List asyncList = asyncService.mediate(listRef);
Promise<Boolean> promise =
asyncService.call(asyncList.contains("badEntry"));
and one that has no return value (void method):
mediator.clear();
Promise<Boolean> promise = asyncService.call();
The text explains that the actual asynchronous invocation
doesn't take place until calling Async.call(), ie:
asyncService.call(asyncList.contains("badEntry"));
^ send ^ prepare
async async
call call
asyncList.clear(); // prepare async call
asyncService.call(); // send async call
Why did you choose this design, and did not choose to
let a proxied method send its own async call?
Best regards
Mike
_______________________________________________
OSGi Developer Mail List
[email protected]
https://mail.osgi.org/mailman/listinfo/osgi-dev
_______________________________________________
OSGi Developer Mail List
[email protected]
https://mail.osgi.org/mailman/listinfo/osgi-dev
_______________________________________________
OSGi Developer Mail List
[email protected]
https://mail.osgi.org/mailman/listinfo/osgi-dev