OASIS Mailing List ArchivesView the OASIS mailing list archive below
or browse/search using MarkMail.

 


Help: OASIS Mailing Lists Help | MarkMail Help

sca-j message

[Date Prev] | [Thread Prev] | [Thread Next] | [Date Next] -- [Date Index] | [Thread Index] | [List Home]


Subject: Re: [sca-j] Another early morning brainstorm - conversations revisited


Hi Simon,

Comments (with drawings this time!) inline.

Jim

On Aug 11, 2008, at 7:31 AM, Simon Nash wrote:

<snip/>

<scn3>I understand your point about JMS.  However, you haven't addressed my other point about the conversational provider instance needing to be created and initialized before further invocations are made on it.  For transports that provide reliable queued in-order delivery of messages (the JMS case) the transport can take care of this.  For other transports, the first invocation must execute and complete before the second one can occur.  This serialization needs to be handled somehow, either by the application or by the infrastructure.</scn3>

Sorry, I  didn't explain well. Let me try with an example. The current programming model will work fine in the case you outline above, that is, when an invocation arrives out of order as long as the creation of the conversation id is a synchronous event from the perspective of the client reference proxy. Let's start by taking a simplistic SCA implementation and look at the sequence of events that would happen:

1. Application code invokes a reference proxy that represents a conversational service by calling the orderApples operation
2. The reference proxy knows a conversation has not been started so acquires a write lock for the conversation id and *synchronously* performs some work to get a conversation id. Since this is a simplistic implementation, it generates a UUID. and caches it, then releases the write lock. From this point on, the conversation id is available to the reference proxy.
3. The reference proxy then invokes the orderApples operation over some transport, flowing the id and invocation parameters
4. The reference proxy returns control to the application logic
5. At some later time, the orderApples invocation arrives in the runtime hosting the target service
6. If the target instance is not created or initialized, the runtime does so
7. The runtime dispatches the orderApples invocation to the target instance.

Now let's assume the client invokes both orderApples and orderPlums in that order, which are non blocking. Let's also assume ordered messaging is not used (e.g. the dispatch is in-VM using a thread pool) and for some reason orderPlums is delivered to the target runtime before orderApples. Steps 1-4 from above remain the same. Then:

5. The application logic invokes orderPlums.
6. The reference proxy acquires a read lock to read the conversation id which it obtains immediately. It then releases the lock and flows the invocation data and id over some transport.
7. The orderPlums request arrives, along with the conversation id created in step 2 before the orderApples invocation.
8. The target instance does not exist, so the runtime instantiates and initializes it
9. The orderApples invocation containing the operation parameters and same conversation id arrives on the target runtime . At this point the target instance is already created so the runtime dispatches to it.

Note that the read/write lock would not be necessary for clients that are stateless components since they are thread-safe.

In the above cases, the creation of a conversation id is orthogonal to the creation of a provider instance and the former always completes prior to an instance being created. If we were to replace the conversation generation algorithm (UUID) with something that was more complex (e.g. called out to the service provider runtime) the same sequence would hold.

Also, the above sequence solves the problem of using conversational services with transacted messaging that arises by forcing a request-reply pattern to be part of the forward service contract.

<scn4>Now it's my turn to apologize for not explaining well enough.  The sequence above covers the case where either of the two forward calls can validly execute first on the same conversational instance.  The case I am concerned about is where the first call must execute before the second call because it does some necessary prerequisite initialization of the conversational instance.  For example, the first call is "create order" and the subsequent calls add items (apples, plums) to the order that was previously created.  I believe this will be the common case, and in this case it is necessary for the first call in the conversation to be a two-way call.</scn4>


In your example, application initialization logic ("create order") should be performed in an @Init method on the service provider and not be left to the client. Generally, this is because initialization is associated with component lifecycle, which is part of the contract an implementation has with the SCA runtime, not a client.

<scn5>If the initialization needs any information from the client, it would need to happen as part of a client business method call.</scn5>


That's what I mentioned below.


There may, however, be a requirement for a particular service operation to be invoked before others. This may be to supply data needed to initialize a business process. This is very different than initializing a component instance and should be modeled as part of the service contract since it is a requirement placed on clients, not the SCA runtime.
 
If a business process needs to be initialized, it should be handled through the following steps:

1. Telling developers to write code that always calls a particular operation first. This can be done in a variety of ways such as through documentation, some type of metadata on the service operation the runtime can use to enforce sequencing, verbally, or not being considerate and just having the service provider throw an error at runtime.

2. Having the first operation be synchronous and throwing a fault if other operations are invoked prior to it. Or, if the operations are asynchronous, using a transport that guarantees ordered delivery.

If a business process needs to be initialized, the service implementation still must guard against a client inadvertently calling operations out of sequence, even if the SCA runtime provides some sort of mechanism on the client side to enforce sequencing such as proprietary operation metadata. This is because a service may be invoked from a non-SCA client if it is bound. If however, it is instance-related initialization, the service implementation need not take any precaution as @Init will always ensure initialization happens before any other operation.

<scn5>I agree that all these approaches are possible.  My main point was that this is a common pattern, and we seem to be agreeing on that.</scn5>


It is but I think it we need to be careful and distinguish between the two, as they are very different. They will place unique requirements on application code and we need to be clear about those differences.


<snip/>


<scn4>I haven't been able to explain my concern well enough.  Let me lay it out using sequences of steps.  Sequence A is what the client application expects will happen,  Sequence B is what may actually happen in some cases.

Sequence A:
1. The client business logic calls a proxy to start a conversation.
2. The proxy interacts with some system facility to obtain a conversation ID.
3. The proxy invokes the service.
4. The proxy returns to the client.
5. The client business logic obtains the conversation ID from the proxy.
6. The client business logic creates a correlation entry keyed by the conversation ID, for use by callbacks.
7. The service business logic executes and makes a callback to the client.
8. The client callback business logic uses the conversation ID passed by the server to lookup the correct information in the correlation table.



Sequence B:
1. The client business logic calls a proxy to start a conversation.
2. The proxy interacts with some system facility to obtain a conversation ID.
3. The proxy invokes the service.
7. The service business logic executes and makes a callback to the client.
8. The client callback business logic uses the conversation ID passed by the server to lookup the correct information in the correlation table.
4. The proxy returns to the client.
5. The client business logic obtains the conversation ID from the proxy.
6. The client business logic creates a correlation entry keyed by the conversation ID, for use by callbacks.

If steps 7 and 8 happen before steps 4, 5 and 6, the client logic is broken.  The only way to guard against this is to move steps 5 and 6 much earlier, and change what step 2 does, as follows:

Sequence C:
5. The client business logic obtains a conversation ID from the system infrastructure.
6. The client business logic creates a correlation entry keyed by the conversation ID, for use by callbacks.
2. The client business logic associates this conversation ID with a proxy for the service.
1. The client business logic calls a proxy to start a conversation.
3. The proxy invokes the service.
7. The service business logic executes and makes a callback to the client.
8. The client callback business logic uses the conversation ID passed by the server to lookup the correct information in the correlation table.
4. The proxy returns to the client.

Steps 5 and 2 in sequence C require either new APIs or new semantics for existing APIs.  This negates the apparent simplicity advantage of your proposed approach.</scn4>



Thanks for laying this out. Sorry in advance if I've misunderstood what you are getting at so please correct me if I'm missing something obvious.

Unfortunately, I don't think your approach stops Sequence B from happening unless you are imposing the arbitrary requirement that callbacks cannot be made until a sequence of events happen on the client (i.e. it obtains a conversation id and stores it somewhere). If a service provider makes a callback during an initial synchronous call, and if the callback is non-blocking, it may arrive before the forward call returns. If the callback is synchronous, it is guaranteed to arrive before the forward call returns. This means Sequence B may happen in your model as well.

<scn5>By "my approach", do you mean the proposal that started this thread?  This is equivalent to sequence C, with step 5 representing the initial call and step 6 representing logic that the client would perform after this call.  The initial call that sets up the conversation would not make any callbacks, as you say.  My point here is that sequence C is required, and we need to find some way to guarantee it.</scn5>


Again, I believe my approach fits perfectly with Sequence C in a number of scenarios and yours does only if the application makes certain guarantees. I'll break my explanation down into a description of what I think your approach requires and then move to why it is problematic. 

Specifically, by "your approach" I mean a new programming model that:

1. Requires a forward/request response to be part of the *service* operation

2. Requires that forward request/response operation to return either a genericized CallableReference or the service contract, i.e.

public interface OrderService {

   CallableReference<OrderService> startConversation();
  
   //....
}

 -- OR --

public interface OrderService {

   OrderService startConversation();

   // ....
}

   
3. Requires the forward request/response service operation to be invoked first by the client

4. Requires that forward request/response pattern to involve executing a service provider implementation instance operation 

5. Requires the client make all subsequent invocations for the same conversation to be performed through the proxy returned by startConversation();

6. Requires that a conversation is never initiated to OrderService using a transacted messaging provider

7. Requires the service provider implementation not to make a callback in startConversation() as well as not make a callback until the client has stored the conversational id (more on the latter below)

I believe all of these restrictions are unnecessary, except number 7 in very specific circumstances.  Besides the unnecessary restrictions described above, here's a slightly more detailed example of why I believe the approach you are advocating is problematic. 



In order for Sequence C to be guaranteed, one of two approaches can be taken:

Approach 1: The Application Handles It 
---------------------------------------------------------
Application code must be written in such a way as to guarantee that no callback is issued until the *client* has finished certain operations. Note in many cases this is *not* strictly when the client has received a response from a forward synchronous request/response operation to initiate a conversation. For example, consider the following:

A---sync--->B
                     B---async--->C
A<---sync---B
                     B<---cb--------C
A<---cb----- B
A (persist conversation id)

One way for application code to guard against this is by placing a restriction that the first B operation not introduce callouts which may result in a callback to A anywhere along the code execution path until A has persisted the conversation id. This may be done by placing a restriction on the service contract semantics that callbacks to A cannot be done until all work has completed for the first B operation and B receives a second operation invocation from A. Waiting for the second operation invocation is important since B may callback before A has had the chance to persist the conversation id.

How would your approach solve this differently? If this restriction (number 7 above) is required, then I don't see the necessity for all operations initiating a conversation to be synchronous request/response.

Approach 2: Transacted Messaging 
---------------------------------------------------------
Transacted messaging is used. In order for this to happen, the initial call would need to be non-blocking. If the initial interaction between A and B and persisting the conversation id is performed transactionally, the following sequence can be guaranteed:
 
Trx start
A---asyn---> MS (message store) 
A (persist conversation id)
Trx commit
                      MS--------->B   
                                          B---async--->C
                                          B<---cb--------C
A<---cb---------------------B    (note this invocation would go through a MS but I omitted it for brevity)


Both our models will work with Approach 1. However, yours introduces the 7 restrictions I mentioned above, significantly changes the Java SCA model, and will not work with Approach 2. The alternative I am outlining preserves the existing model with the addition of one API (the ability to get the conversation id from the ComponentContext) and works with both approaches. Moreover, it only places the callback restriction on applications when transacted messaging is not used.


The issue here is not the two programming models but rather bad programming. Both the client and service provider implementations have failed to properly design their service contracts and account for callbacks (and potentially asynchrony as well). One way to guard against this scenario from happening is to deal with it at the application level. This is what you effectively show in Sequence C. There are also two other simpler ways to handle this problem, one at the application level, the other through facilities entirely provided by the runtime (and not left to application code).
Sequence B can be avoided in my model at the application level by having the service semantics (the contract between the client and provider) prohibit callbacks until a particular forward service operation has been invoked.

<scn5>This is complex to enforce, because SCA would need to override the normal transport-level dispatching semantics.  It also does not work, because of the need for steps 5 and 6 to occur before step 8.  The semantics you are proposing would ensure that step 8 happens after step 4, but they would not ensure that step 8 happens after steps 5 and 6.  There is nothing that infrastructure can reasonably do to ensure that step 8 happens after steps 5 and 6, because it does not know when step 6 has completed.</scn5>


Sorry, I don't get what you mean by "SCA would need to override the normal transport-level dispatching semantics"? I have a feeling we are not on the same page with something very fundamental. Guaranteeing steps 5 and 6 happen before 8 can be done in the following cases:

1. A synchronous request/reply initiating a conversation
2. A non-blocking request initiating a conversation
3. A non-blocking request initiating a conversation using transacted messaging

In all cases, the proxy generates (or delegates the generation of the conversation id) synchronously prior to the first invocation on a service provider instance. This is transparent to application code.

Case 1
----------------
This case requires the provider implementation not make any invocations that result in a callback to the client between steps a-i.



Client Proxy Id Generator Binding Provider                    Endpoint Host                   Service Provider Instance    
       |                       |
 (a) |---------------> |
|(b)-----sync-------->|
|(c)<----sync---------|
|(d)-------------------------------------------->|
|(e)<---------------------------------------------|  
                                                                                                    |------------------------------------>|                                                    |
                                                                                                    |                                                  |----------------------------------->(f)| 
                                                                                                    |                                                  |<--------------------------------------|
                                                                                                    |<------------------------------------|
                           (g)|<-------------------------------------------------|
       |(h)<------------|                                                                    |
  (i) |---------------->|                                                                   |                                                  |                                                     |
       |                        |------------------------------------------------>|                                                  |                                                     |
       |                                                                                            |------------------------------------>|                                                    |
       |                                                                                            |                                                  |----------------------------------->(j)| 
       |                                                                                            |                                                  |<--------------------------------------|
       |                                                                                            |<------------------------------------|
       |                   (k)|<-------------------------------------------------|
  (l) |<----------------|                                                                    |

One way to express this in code is:

Response someAppResponse = forwardService.startConversation();   // steps a-h
// get the conversation id, e.g. componentContext.getConversation(forwardReference); 
// store the conversation id in a database 
forwardService.proceed();  // step i


Case 2
----------------

In the scenario I outlined where application code does not issue a callback until a certain operation is called (step i):
Client Proxy Id Generator Binding Provider                    Endpoint Host                   Service Provider Instance    
       |                       |
 (a) |---------------> |
|(b)-----sync-------->|
|(c)<----sync---------|
|(e)-------------------------------------------->|
|(f)<---------------------------------------------|  
       |(g)<------------|                                                                   |
       |                                                                                           |-------------------------------->(h)|
       |                                                                                           |                                                  |-------------------------------------->|
  (i) |---------------->|                                                                   |                                                  |                                                     |
       |                        |------------------------------------------------>|                                                  |                                                     |
       |                                                                                            |------------------------------------>|                                                    |
       |                                                                                            |                                                  |----------------------------------->(j)| 
       |                                                                                            |                                                  |<--------------------------------------|
       |                                                                                            |<------------------------------------|
       |                   (k)|<-------------------------------------------------|
  (l) |<----------------|                                                                    |

The Id Generator may be anywhere in the domain - e.g. local to the proxy or remote, it is implementation-specific - and uses an algorithm that generates unique ids over space and time. Steps a-g are done synchronously from the perspective of the client. Between steps b-c, the SCA domain infrastructure may perform other tasks, such as creating some kind of context, as long synchrony is maintained between a-g. The client performs correlation activity between steps g and i. 

As long as steps a-g happen before h, and application code does not issue any callback until after j, Sequence C will be guaranteed. The important thing here is is that step h does not need to be synchronous.

One way to express this in code is - not it does not change from Case 1 except the first invocation is non-blocking and returns void:

forwardService.startConversation();   // steps a-g
// get the conversation id, e.g. componentContext.getConversation(forwardReference); 
// store the conversation id in a database 
forwardService.proceed();  // step i



Case 3
------------------
A transacted messaging scenario that grouped the two operation invocations would alter Case 2 by not sending the messages until transaction commit:


Client  Proxy Id Generator Binding Provider                    Endpoint Host                   Service Provider Instance    

(transaction start)
       |                       | 
       |                       |
 (a) |---------------> |
|(b)-----sync-------->|
|(c)<----sync---------|
|(e)-------------------------------------------->|
|(f)<---------------------------------------------|  
       |(g)<------------|                                                                   |
  (i) |---------------->|                                                                   |                                                  |                                                     |
       |                        |------------------------------------------------>|                                                  |                                                     |
(transaction commit)                                                               |                                                  |                                                     |
       |                                                                                            |-------------------------------->(h)|                                                     |
       |                                                                                            |                                                  |-------------------------------------->|
       |                                                                                            |------------------------------------>|                                                     |
       |                                                                                            |                                                  |----------------------------------->(j)| 
       |                                                                                            |                                                  |<--------------------------------------|
       |                                                                                            |<------------------------------------|
       |                   (k)|<-------------------------------------------------|
  (l) |<----------------|                                                                    |

From an application perspective, transacted messaging makes life easy as the former does not need to restrict when callbacks are made. It also provides the added benefit that storing of the correlation id and messaging enqueuing are done transactionally. If this were not the case, the client would be left in an inconsistent state (i.e. enqueuing succeeds but the conversation id save fails).

One way to express this in code is:

forwardService.startConversation();   // steps a-g
// get the conversation id, e.g. componentContext.getConversation(forwardReference); 
// store the conversation id in a database 
forwardService.proceed();  // step i

The only change from Case 1 is the first invocation is non-blocking and returns void. The only change from Case 2 is both invocations are not sent until the transaction commits.

In neither of these three cases is there a change to the transport level semantics. For example, in JMS terms, messages can be sent using auto acknowledge or transacted.



 This does not necessarily have to be the first operation. In addition, callback methods would need to check against a callback being issued incorrectly (i.e. before the particular forward call), reporting an exception or taking some other action.

For example, a client may wish to store a conversation id in a database and issue a forward "proceed" invocation, as in:

forwardService.startConversation();
// get the conversation id this will vary based on Simon's or my approach, so it is commented out
// store the conversation id in a database
forwardService.proceed();

If the service provider does not issue a callback until the proceed operation happens, the code will work correctly. If a callback is issued before proceed is called, an exception should be issued as it is a programming error. Performing the check to ensure a callback is not performed prematurely is a trivial operation. This would involve performing a null check when correlating the conversation id, which should be done anyway.

<scn5>You are now effectively describing the approach I proposed at the start of this thread, with an additional synchronous forward call.</scn5>

No, I do not believe I am. Application code looks very different in my approach:

forwardService.startConversation(); 
// get the conversation id, e.g. componentContext.getConversation(forwardReference); 
// store the conversation id in a database 
forwardService.proceed(); 

As I outlined in the above three cases, startConversation may be synchronous or one-way. Also, note the "proceed" operation is application-specific and not mandated by what I am proposing. It may be sync or one-way - whatever makes sense for the application. 


Another example would be:


public interface OrderService {
   
@OneWay
void orderApples(int qty);

@OneWay
void orderPlums(int qty);

         @EndsConversation
 @OneWay
 void placeOrder();

}
 
 



My approach can also be used in conjunction with policy to have the runtime guarantee Sequence B does not happen. If we have the above use transacted messaging, where a transaction is begun prior to startConversation() and committed after proceed(), there is no possibility that Sequence B can happen. This is because the messages will not be received by the provider until the transaction commits, which happens after the conversation id has been persisted (either to a database or some in-memory holder). I'll also note that in order for the conversation id to be obtained using a messaging provider there must be a mechanism that is not tied to message delivery and hence a separate API invocation. Fortunately, this can be as easy as doing the following after the "startConversation()" invocation above:

componentContext.getConversation(forwardService);

Of course this is not to say my approach requires a transactional messaging provider. Recalling the previous option, suitable measures can be taken at the application level to avoid Sequence B from happening in a very simple runtime.

<scn5>The use of transacted messaging requires a series of one-way calls with no callbacks until after the transadtion has committed.  This is very different from the scenario we have been discussing, with two-way operations that may invoke callbacks.</scn5>


I thought we were discussing conversations and callbacks? I would argue that we need to look at this holistically, with particular attention to key use cases, which I believe transacted messaging represents. 
 

Given this, I still believe my "simplicity advantage" holds while avoiding the perils of Sequence B, either by taking appropriate measures at the application level or through runtime enforcement via policy (on runtimes that can take advantage of transacted messaging).  Moreover, I would argue the key issue we need to consider with the two approaches is whether each supports the range of requirements typical applications are likely to place on an SCA runtime. In this context, the approach I am advocating works with transacted messaging, whereas requiring a synchronous request-response forward operation to commence a conversation does not. In my experience, loosely coupled, transacted messaging is a cornerstone of enterprise applications and we should not be codifying an API in SCA that prohibits its use.

<scn5>The transacted messaging case should be considered, but I don't think it should drive the whole discussion.  As I said above, it makes some very specific assumptions about the interaction between service and client.  I don't think this is a very common case amongst all the situations where SCA conversations and callbacks could be used.  We might be able to fine tune my proposal to provide a solution for this aspect, while leaving the other parts of the approach in place.</scn5>


I completely and fundamentally disagree with this. Transacted messaging is a cornerstone of many enterprise applications. I believe it must be enabled by SCA and should be one of the principal patterns that drives discussion. For example, without a good solution, I will not be able to use SCA (at least without proprietary extensions) on several financial services projects I am working on. I suspect any application that requires reliable messaging in high-throughput environments will be in the same position. Of all the Java-based programming models running around in the wild that claim to be "enterprise-class," I can't think of one that fails to properly support transacted messaging. 

To the point about transacted messaging making specific assumptions regarding  the interaction between client and provider, I would argue this is likely to be the case with many, if not all, transports. 

Again, I'm biased, but I believe the approach I outlined handles the transacted messaging case as well as the others you highlight with minimal changes and disruption to the specification. It also places less restrictions on application code, uses minimal APIs, bleeds much less SCA into application code, and requires less test infrastructure. In this respect, I believe it is much simpler and less disruptive, while accommodating a wider range of important scenarios. I'd prefer we start with the path requiring less change and restrictions on application code unless there is something fundamentally wrong that cannot be fixed. 
   
Jim



[Date Prev] | [Thread Prev] | [Thread Next] | [Date Next] -- [Date Index] | [Thread Index] | [List Home]