-
Notifications
You must be signed in to change notification settings - Fork 27
RPC Features
All RPC APIs are asynchronous by nature (and not "by default", which would suggest the availability of synchronous variants). This is simply because Cap'n Proto is a fundamentally asynchronous protocol, and condemning synchronous RPC "harmful" made its transition to everyday knowledge.
Generated RPC methods return a Task
, encouraging asynchronous programming style. There are countless whitepapers, blog post and tutorials around the Web on .NET asynchronous programming. Hence, these sections just give a brief summary on possible modelling styles.
A method is "trivial" if it is neither computationally intensive, nor does it depend on I/O. Let's say, you implement some lookup service which resolves numeric IDs to object names:
interface SomeInterface
{
lookupId @0 (id: Int32) -> (objectName: Text);
}
The lookup operation is just a dictionary access, thus very fast. Then there is nothing wrong with implementing lookupId
synchronously:
class SomeInterface : ISomeService
{
readonly ConcurrentDictionary<int, string> _table = new ConcurrentDictionary<int, string>();
public void Dispose()
{
}
public Task<string> LookupId(int arg, CancellationToken cancellationToken_ = default)
{
return Task.FromResult(_table[arg]);
}
}
If a method is compute-bound, you might consider applying taks parallel patterns.
interface SatSolver
{
isSatisfiable @0 (cnf: List(UInt32)) -> (sat: Bool);
}
class SatSolver : ISatSolver
{
public void Dispose()
{
}
public async Task<bool> IsSatisfiable(IReadOnlyList<uint> cnf, CancellationToken cancellationToken_ = default)
{
if (cnf == null || cnf.Count >= 32)
throw new ArgumentException();
return await Task.Factory.StartNew(() =>
{
bool haveSolution = false;
var result = Parallel.For(0, 1 << cnf.Count, (i, s) =>
{
uint x = (uint)i;
if (cnf.All(y => ~(x ^ y) != 0))
{
Volatile.Write(ref haveSolution, true);
s.Stop();
}
cancellationToken_.ThrowIfCancellationRequested();
});
return Volatile.Read(ref haveSolution);
}, cancellationToken_, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
}
If a method depends on calling other capabilities (or other I/O) it is recommended to use stricly asynchronous programming style (await
instead of Wait()
). It is also good practice to hand over the cancellation token to the subsequent operations (as long as your transaction semantics admit it).
interface SearchNode
{
find @0 (name: Text) -> (content: Text);
}
class SearchNode: ISearchNode
{
readonly List<ISearchNode> _linkedNodes = new List<ISearchNode>();
public void Dispose()
{
foreach (var node in _linkedNodes)
node.Dispose();
}
public async Task<string> Find(string name, CancellationToken cancellationToken_ = default)
{
if (File.Exists(name))
{
return await File.ReadAllTextAsync(name, cancellationToken_);
}
else
{
var tasks = _linkedNodes.Select(node => node.Find(name, cancellationToken_)).ToList();
await Task.WhenAll(tasks);
return tasks.FirstOrDefault(t => t.IsCompletedSuccessfully && t.Result != null)?.Result;
}
}
}
Callers are allowed to pass null
arguments for capabilities. So any callee must expect null capabilities. The bad news is that there are multiple ways, all of which giving a capability the meaning of "null":
- Null reference. This only happens at the caller's side. If you don't consume a capability interface directly, you won't see that case on the callee's side.
-
Proxy.IsNull
is something you can check when casting received capabilities toProxy
. - A capability which eventually resolves to a null capability. This may happen in promise pipelining scenarios.
Moreover, a capability may break due to several circumstances:
- The method returning the capability threw an exception (which we don't see immediately in case of promise pipelining).
- The method returning the capability was canceled.
- Connection loss to the capability host
The good news is that null capabilities and broken capabilities can be treated equally. They don't hurt as long as you don't call on them. If you do, they throw an RpcException
(with the message text giving details on the circumstances).
It is a reasonable decision not to perform (capability) parameter validation at all: In concurrent environments, any capability might break right after validation anyway. Should you still require validation, there is a special extension method for that purpose:
public static async Task<TInterface?> Unwrap<TInterface>(this TInterface cap) where TInterface: class, IDisposable
Unwrap
turns a capability into a Task
which eventually unveils the validity of the passed capability:
- Returns
null
when the capability resolves to (or already is) the null capability. - Awaiting on it throws an exception when the capability is broken.
- Returns the (potentially resolved) capability in all other cases.
When a callee throws an exception, the Capnp.Net.Runtime
converts that in an excepted method return and serializes the exception's message. The caller code receives an RpcException
with that message. Note that there is currently no way of restoring the original type of exception thrown, since Cap'n Proto simply provides no means for doing that. Although technically possible, this would be a .NET specific extension and would not cooperate with other Cap'n Proto implementations.
Behind the scenes, the exception thrown by CancellationToken.ThrowIfCancellationRequested()
is a TaskCanceledException
. When the Capnp.Net.Runtime
catches it from a callee, it converts that in a canceled method return and reconstructs the canceled state at caller side. I.e. the Task
returned to the caller will have its IsCanceled
property set to true
.
There is a separate Wiki page on this topic.
There is a separate Wiki page on this topic.