OperationContractAttribute.IsOneWay operations aren't simply fire-and-forget.
Although I am responsible for writing a good portion of the programmer documentation for WCF, there are always little things that you "realize" suddenly that you didn't quite understand. Today, I learned from Shy Cohen and Maheshwar Jayaraman (thanks guys!) about OperationContractAttribute.IsOneWay -- the external documentation for which is here and here and is wrong, or certainly not right enough. Sigh. Shall I try again?
The second link typifies my misunderstanding. It says:
A one-way operation is one in which a client invokes an operation and continues processing after WCF queues the message for sending by the client.
Ah... not quite. What I now understand is that IsOneWay operations are really only for the case when you do not require a return message. However, the client object call does not return until the data is succesfully written to the wire -- which means that if the service cannot read the data from the wire that the client object blocks. Let me say this again: clients can block on one-way operations because the client object only returns once channel.Send() returns.
For the most part, the scenario in which a client can block on a oneway call is when a client object makes a large number of one-way calls in a tight loop. For example in the following code the Print() method is a one-way operation.
PrintClient client = new PrintClient();
for (int i = 0; i < 3000; i++) // <= 300 will not block, 3000 will
client.Print("String #" + i.ToString());
Console.WriteLine("Client is done");
In this case, the service's ability to retrieve data off the wire using the default settings is quickly reduced; suddenly the client.Print call begins blocking on the return from the Send() call at the transport level. A loop like the above but with 300 or fewer calls will print out "Client is done" usally before the service can print out that it has received a message. With 3000, however, the service can print out most messages before the client is able to complete the loop.
This behavior has several ramifications that I need to put into the documentation, but I'll write them out here first.
- One-way methods will block in any situation in which the transport cannot send the message. Fire too many one-ways at a service and it could block your client.
- It follows, then, that exceptions can be thrown on a one-way call any time Send does not return. For example, EndpointNotFoundException and SendTimeoutException can easily occur. Test it yourself! :-)
- RM implements its own message buffer, so clients can return for a longer period -- until the RM buffer fills up, in which case the client blocks.
More generally, the way around this problem is to insert buffers that separate the client call from the transport send action. The problem is that any such buffer will have its own limits that you must be aware of. Options include the following.
- Use asynchronous calls against the client object. (Limit: ThreadPool capacity.)
- Use RM. (Limit: RM message buffer.)
- Implement a memory message queue to call against and then make the calls from there. (Limit: queue size.)
In all cases, the buffer you use will still have some limit, especially to protect against denial of service attacks. The big takeaway is:
- If you want to flood out one-way messages, the only way for you to get the proper client behavior in this scenario (a flood of oneway calls) is to test the kind of load you expect and balance both the client and service buffers to properly balance the load.
Of course, if the transport were to change and support a truer fire-and-forget model, such as UDP, you would not encounter this problem because the transport would return from Send once the data was sent regardless of the ability of the service to read the data. There it is. Now, to rewrite the documentation....