forked from microsoft/vs-streamjsonrpc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for marshaling IDisposable objects over RPC
Closes microsoft#532
- Loading branch information
Showing
23 changed files
with
1,613 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# `IDisposable` support | ||
|
||
StreamJsonRpc allows marshaling `IDisposable` objects in arguments and return values. | ||
|
||
An `IDisposable` object may also implement `INotifyDisposable` on the sender side | ||
so the sender can terminate the lifetime of the marshalled object to release resources. | ||
|
||
## Use cases | ||
|
||
In all cases, the special handling of an `IDisposable` value only occurs if the container of that value is typed as `IDisposable`. | ||
This means that an object that implements `IDisposable` will not necessarily be marshaled instead of serialized. | ||
Consider each of the below use cases to see how the value can be *typed* as `IDisposable`. | ||
|
||
For each use case, assume `DisposeAction` is a class defined for demonstration purposes like this: | ||
|
||
```cs | ||
class DisposeAction : IDisposable | ||
{ | ||
private readonly Action disposeAction; | ||
|
||
internal DisposeAction(Action disposeAction) | ||
{ | ||
this.disposeAction = disposeAction; | ||
} | ||
|
||
public void Dispose() => this.disposeAction(); | ||
} | ||
``` | ||
|
||
### Method return value | ||
|
||
In the simplest case, the RPC server returns an `IDisposable` object that gets disposed on the server | ||
when the client disposes it. | ||
|
||
```cs | ||
class RpcServer | ||
{ | ||
public IDisposable GetDisposable() => new DisposeAction(() => { /* The client disposed us */ }); | ||
} | ||
``` | ||
|
||
### Method argument | ||
|
||
In this use case the RPC *client* provides the `IDisposable` value to the server: | ||
|
||
```cs | ||
interface IRpcContract | ||
{ | ||
Task ProvideDisposableAsync(IDisposable value); | ||
} | ||
|
||
IRpcContract client = jsonRpc.Attach<IRpcContract>(); | ||
IDisposable arg = new DisposeAction(() => { /* the RPC server called Dispose() on the argument */}); | ||
await client.ProvideDisposableAsync(arg); | ||
``` | ||
|
||
### Value within a single argument's object graph | ||
|
||
In this use case the RPC client again provides the `IDisposable` value to the server, | ||
but this time it passes it as a property of an object used as the argument. | ||
|
||
```cs | ||
class SomeClass | ||
{ | ||
public IDisposable DisposableValue { get; set; } | ||
} | ||
|
||
interface IRpcContract | ||
{ | ||
Task ProvideClassAsync(SomeClass value); | ||
} | ||
|
||
IRpcContract client = jsonRpc.Attach<IRpcContract>(); | ||
var arg = new SomeClass | ||
{ | ||
DisposableValue = new DisposeAction(() => { /* the RPC server called Dispose() on the argument */}), | ||
}; | ||
await client.ProvideClassAsync(arg); | ||
``` | ||
|
||
While this use case is supported, be very wary of this pattern because it becomes less obvious to the receiver that an `IDisposable` value is tucked into the object tree of an argument somewhere that *must* be disposed to avoid a resource leak. | ||
|
||
### As an argument without a proxy for an RPC interface | ||
|
||
When you are not using an RPC interface and dynamically generated proxy that implements it, you can still pass a marshaled `IDisposable` value as an argument by explicitly passing in the declared parameter types to the `InvokeWithCancellationAsync` call: | ||
|
||
```cs | ||
IDisposable arg = new DisposeAction(() => { /* the RPC server called Dispose() on the argument */}); | ||
await jsonRpc.InvokeWithCancellationAsync( | ||
"methodName", | ||
new object?[] { arg }, | ||
new Type[] { typeof(IDisposable) }, | ||
cancellationToken); | ||
``` | ||
|
||
### Invalid cases | ||
|
||
Here are some examples of where an object that implements `IDisposable` is serialized (i.e. by value) instead of being marshaled (i.e. by reference). | ||
|
||
In this example, although `Data` implements `IDisposable`, its declared parameter type is `Data`: | ||
|
||
```cs | ||
class Data : IDisposable { /* ... */ } | ||
|
||
IDisposable arg = new Data(); | ||
await jsonRpc.InvokeWithCancellationAsync( | ||
"methodName", | ||
new object?[] { arg }, | ||
new Type[] { typeof(Data) }, | ||
cancellationToken); | ||
``` | ||
|
||
Here is the same situation with an RPC interface: | ||
|
||
```cs | ||
class Data : IDisposable { /* ... */ } | ||
|
||
interface IRpcContract | ||
{ | ||
Task ProvideDisposableAsync(Data value); | ||
} | ||
|
||
IRpcContract client = jsonRpc.Attach<IRpcContract>(); | ||
Data arg = new Data(); | ||
await client.ProvideDisposableAsync(arg); | ||
``` | ||
|
||
Or similar for an object returned from an RPC server method: | ||
|
||
```cs | ||
class Data : IDisposable { /* ... */ } | ||
|
||
class RpcServer | ||
{ | ||
public Data GetDisposableData() => new Data(); | ||
} | ||
``` | ||
|
||
In each of these cases, the receiving part will get a `Data` object that implements `IDisposable`, but calling `Dispose` on that object will be a local call to that object rather than being remoted back to the original object. | ||
|
||
## Resource leaks concerns | ||
|
||
When an `IDisposable` instance is sent over RPC, resources are held by both parties to marshal interactions | ||
with that object. | ||
|
||
These resources are released when any of these occur: | ||
|
||
1. The receiver calls `IDisposable.Dispose()` on the object. | ||
1. The JSON-RPC connection is closed. | ||
|
||
To enable the sender to terminate the connection with the proxy to release resources, the sender should call `JsonRpc.MarshalWithControlledLifetime<T>` to wrap the `IDisposable` before sending it. | ||
|
||
## Protocol | ||
|
||
The protocol for proxying a disposable object is based on [general marshaled objects](general_marshaled_objects.md) with: | ||
|
||
1. A camel-cased method name transform applied. | ||
1. Lifetime of the proxy controlled by the receiver. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# General marshaled objects | ||
|
||
Passing objects over RPC by value is the default and preferred means of exchanging information. | ||
Objects passed by value have no relationship between sender and receiver. | ||
Any methods defined on the object execute locally in the process on which they were executed and do not result in an RPC call. | ||
Objects passed by value are *serialized* by the sender with all their member data and deserialized by the receiver. | ||
|
||
When objects do not have a serialized representation (e.g. they contain no data) but do expose methods that should be invokable by a remote party in an RPC connection, that object may be _marshaled_ instead of serialized. | ||
When an object is marshaled, a handle to that object is transmitted that allows the receiver to direct RPC calls to that particular object. | ||
A marshaled object's lifetime is connected between sender and receiver, and will only be released when either sender or receiver dispose the handle or the overall RPC connection. | ||
|
||
To the receiver, a marshaled object is represented by an instance of the marshaled interface. | ||
The implementation of that interface is an RPC proxy that is *very* similar to ordinary [dynamic proxies](dynamicproxy.md) that may be generated for the primary RPC connection. | ||
This interface has the same restrictions as documented for these dynamic proxies. | ||
|
||
**CONSIDER**: Will interfaces with events defined on them behave as expected, or should we disallow events on marshaled interfaces? | ||
|
||
Marshaled objects may _not_ be sent in arguments passed in a notification. | ||
An attempt to do so will result in an exception being thrown at the client instead of transmitting the message. | ||
This is because notification senders have no guarantee the server accepted and processed the message successfully, making lifetime control of the marshaled object impossible. | ||
|
||
## Use cases | ||
|
||
When preparing an object to be marshaled, only methods defined on the given interface are exposed for invocation by RPC. | ||
Other methods on the target object cannot be invoked. | ||
|
||
Every marshaled object's proxy implements `IDisposable`. | ||
Invoking `IDisposable.Dispose` on a proxy transmits a `dispose` RPC notification to the target object and releases the proxy. | ||
|
||
See [additional use cases being considered](general_marshaled_objects_2.md) for general marshalling support. | ||
|
||
## Protocol | ||
|
||
Any marshaled object is encoded as a JSON object with the following properties: | ||
|
||
- `__jsonrpc_marshaled` - required | ||
- `handle` - required | ||
- `lifetime` - optional | ||
|
||
The `__jsonrpc_marshaled` property is a number that may be one of the following values: | ||
|
||
Value | Explanation | ||
--|-- | ||
`1` | Used when a real object is being marshaled. The receiver should generate a new proxy that directs all RPC requests back to the sender referencing the value of the `handle` property. | ||
`0` | Used when a marshaled proxy is being sent *back* to its owner. The owner uses the `handle` property to look up the original object and use it as the provided value. | ||
|
||
The `handle` property is a signed 64-bit number. | ||
It SHOULD be unique within the scope and duration of the entire JSON-RPC connection. | ||
A single object is assigned a new handle each time it gets marshaled and each handle's lifetime is distinct. | ||
|
||
The `lifetime` property is a string that MAY be included and set to one of the following values: | ||
|
||
Value | Explanation | ||
--|-- | ||
`"call"` | The marshaled object may only be invoked until the containing RPC call completes. This value is only allowed when used within a JSON-RPC argument. No explicit release using `$/releaseMarshaledObject` is required. | ||
`"explicit"` | The marshaled object may be invoked until `$/releaseMarshaledObject` releases it. **This is the default behavior when the `lifetime` property is omitted.** | ||
|
||
### Marshaling an object | ||
|
||
Consider this example where `SomeMethod(int a, ISomething b, int c)` is invoked with a marshaled object for the second parameter: | ||
|
||
```json | ||
{ | ||
"jsonrpc": "2.0", | ||
"id": 1, | ||
"method": "SomeMethod", | ||
"params": [ | ||
1, | ||
{ "__jsonrpc_marshaled": 1, "handle": 5 }, | ||
3, | ||
] | ||
} | ||
``` | ||
|
||
If the RPC server returns a JSON-RPC error response (for any reason), all objects marshalled in the arguments of the request are released immediately to mitigate memory leaks since the server cannot be expected to have recognized the arguments as requiring a special release notification to be sent from the server back to the client. | ||
|
||
### Invoking a method on a marshaled object | ||
|
||
The receiver of the above request can invoke `DoSomething` on the marshaled `ISomething` object with a request such as this: | ||
|
||
```json | ||
{ | ||
"jsonrpc": "2.0", | ||
"id": 15, | ||
"method": "$/invokeProxy/5/DoSomething", | ||
"params": [] | ||
} | ||
``` | ||
|
||
### Referencing a marshaled object | ||
|
||
The receiver of the marshaled object may also "reference" the marshaled object by using `__jsonrpc_marshaled: 0`, | ||
as in this example: | ||
|
||
```json | ||
{ | ||
"jsonrpc": "2.0", | ||
"id": 16, | ||
"method": "RememberWhenYouSentMe", | ||
"params": [ | ||
1, | ||
{ "__jsonrpc_marshaled": 0, "handle": 5 }, | ||
3, | ||
] | ||
} | ||
``` | ||
|
||
### Releasing marshaled objects | ||
|
||
Either side may terminate the marshalled connection in order to release resources by sending a notification to the `$/releaseMarshaledObject` method. | ||
The `handle` named parameter (or first positional parameter) is set to the handle of the marshaled object to be released. | ||
The `ownedBySender` named parameter (or second positional parameter) is set to a boolean value indicating whether the party sending this notification is also the party that sent the marshaled object. | ||
|
||
```json | ||
{ | ||
"jsonrpc": 20, | ||
"method": "$/releaseMarshaledObject", | ||
"params": { | ||
"handle": 5, | ||
"ownedBySender": true, | ||
} | ||
} | ||
``` | ||
|
||
One or both parties may send the above notification. | ||
If the notification is received after having sent one relating to the same handle, the notification may be discarded. | ||
|
||
### Referencing of marshaled objects after release | ||
|
||
When an RPC _request_ references a released marshaled object, the RPC server SHOULD respond with a JSON-RPC error message containing `error.code = -32001`. | ||
|
||
When an RPC _result_ references a released marshaled object, the RPC client library MAY throw an exception to its local caller. | ||
|
||
### No nesting of marshaled object lifetimes | ||
|
||
Although object B may be marshaled in RPC messages that target marshaled object B, marshaled object B's lifetime is _not_ nested within A's. | ||
Each marshaled object must be indepenently released. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# Spec proposal: General marshaled objects | ||
|
||
## Use cases | ||
|
||
**The following content would belong to general_marshaled_objects.md, but StreamJsonRpc does not yet support these use cases.** | ||
|
||
### Marshaling an object in an RPC argument with a lifetime scoped to the method call | ||
|
||
Marshaled objects passed in arguments in _requests_ (not notifications) may be scoped to live only as long as that method call. | ||
This is the simplest model and requires no special handling to avoid resource leaks. | ||
|
||
To marshal an object instead of serialize its value, wrap the object with a marshalable wrapper using the | ||
`T JsonRpc.MarshalLimitedArgument<T>(T marshaledObject)` static method. | ||
|
||
```cs | ||
ISomethingInteresting anObject; | ||
await jsonRpc.InvokeAsync( | ||
"DoSomething", | ||
new object[] | ||
{ | ||
1, | ||
JsonRpc.MarshalLimitedArgument(anObject), | ||
3, | ||
} | ||
) | ||
``` | ||
|
||
The above will allow methods defined on the `ISomethingInteresting` interface to be invoked on `anObject` by the RPC server's `DoSomething` method. | ||
After `DoSomething` returns, `anObject` can no longer be invoked by the RPC server. | ||
No explicit release is required. | ||
|
||
### Marshaling an object with an independently controlled lifetime | ||
|
||
Marshaled objects passed in return values or in arguments where they must remain marshaled beyond the scope of the RPC request can do so. | ||
This mode requires that the sender and/or receiver explicitly release the marshaled object from its RPC connection in order to reclaim resources. | ||
|
||
Use the `IRpcMarshaledContext<T> JsonRpc.MarshalWithControlledLifetime<T>(T marshaledObject)` static method prepare an object to be marshaled. | ||
The resulting `IRpcMarshaledContext<T>` contains both the object to transmit and a method the sender may use to later terminate the marshaling relationship. | ||
|
||
In the following example, an `IDisposable` object is marshaled via a return value from an RPC method. | ||
Notice how the server holds onto the `IRpcMarshaledContext<T>` object so it can release the marshaled object when the subscription is over. | ||
|
||
```cs | ||
class RpcServer | ||
{ | ||
private readonly Dictionary<Subscription, IRpcMarshaledContext<IDisposable>> subscriptions = new Dictionary<Subscription, IRpcMarshaledContext<IDisposable>>(); | ||
|
||
public IDisposable Subscribe() | ||
{ | ||
var subscription = new Subscription(); | ||
var marshaledContext = JsonRpc.MarshalWithControlledLifetime<IDisposable>(subscription); | ||
lock (this.subscriptions) | ||
{ | ||
this.subscriptions.Add(subscription, marshaledContext); | ||
} | ||
|
||
return marshaledContext.Proxy; | ||
} | ||
|
||
// Invoked by the RpcServer when the subscription is over and can be closed. | ||
private void OnSubscriptionCompleted(Subscription subscription) | ||
{ | ||
IRpcMarshaledContext<IDisposable> marshaledContext; | ||
lock (this.subscriptions) | ||
{ | ||
marshaledContext = this.subscriptions[subscription] | ||
this.subscription.Remove(subscription); | ||
} | ||
|
||
marshaledContext.Dispose(); | ||
} | ||
} | ||
``` | ||
|
||
When an object that would otherwise be marshaled is returned from an RPC server method that was invoked by a notification, no marshaling occurs and no error is raised anywhere. | ||
|
||
### Sending a marshaled object's proxy back to its owner | ||
|
||
The receiver of a marshaled object *may* send the proxy they receive back to its originator. | ||
This may be done within an argument or return value. | ||
When this is done, the receiver of that message (i.e. the original marshaled object owner) will receive a reference to the original marshaled object. | ||
|
||
### Invoking or referencing a discarded marshaled object | ||
|
||
When a method on a marshaled proxy is invoked or the proxy is referenced in an argument of an outbound RPC request *after* it has been disposed of, an `ObjectDisposedException` is thrown. | ||
|
||
When the local proxy has not been disposed of but the marshaled object owner has released the target object, a `RemoteInvocationException` may be thrown with the `ErrorCode` property set to `JsonRpcErrorCode.NoMarshaledObjectFound`. |
Oops, something went wrong.