Skip to content

Commit 083e2af

Browse files
v0.12.1-beta (Cancellation Tokens) (#23)
* Added language about how to use cancellation tokens * Additional updates to action results, actions, input object docs * Updated query timeout defaults
1 parent 36de411 commit 083e2af

File tree

4 files changed

+81
-22
lines changed

4 files changed

+81
-22
lines changed

docs/advanced/subscriptions.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ sidebar_label: Subscriptions
66

77
## Initial Setup
88

9-
Successfully handling subscriptions in your GraphQL AspNet server can be straight forward for single server environments or very complicated for multi-server and scalable solutions. First we'll look at adding subscriptions for a single server.
9+
Successfully handling subscriptions in your GraphQL AspNet server can be straight forward for single server environments or very complicated for multi-server and scalable solutions. First we'll look at adding subscriptions for a single server.
1010

1111
### Install the Subscriptions Package
12+
1213
The first step to using subscriptions is to install the subscription server package.
1314

1415
```Powershell
@@ -23,7 +24,6 @@ You must configure web socket support for your Asp.Net server instance separatel
2324

2425
After web sockets are added to your server, add subscription support to the graphql registration.
2526

26-
2727
```C#
2828
// startup.cs
2929
@@ -47,6 +47,7 @@ After web sockets are added to your server, add subscription support to the grap
4747
> Don't forget to add `UseWebsockets` in the `Configure` method of startup.cs
4848
4949
### Create a Subscription
50+
5051
Declaring a subscription is the same as declaring a query or mutation on a controller but with `[Subscription]` and `[SubscriptionRoot]` attributes.
5152

5253
```C#
@@ -63,11 +64,13 @@ public class SubscriptionController : GraphController
6364
}
6465
}
6566
```
67+
6668
> Subscriptions can be asyncronous and return a Task<IGraphActionResult> as well.
6769
6870
Here we've declared a new subscription the server will respond to, one that takes in a `filter` parameter to restrict the data that any subscribers receive.
6971

7072
A query to invoke this subscription may look like this:
73+
7174
```javascript
7275
subscription {
7376
onWidgetChanged(filter: "Big"){
@@ -77,12 +80,13 @@ subscription {
7780
}
7881
}
7982
```
83+
8084
Any updated widgets that start with the phrase "Big" will then be sent to the requestor as they are changed on the server.
85+
8186
### Publish a Subscription Event
8287

8388
In order for the subscription server to send data to any subscribers it has to be notified when something changes. It does this via named Subscription Events. These are internal, unique keys that identify when something happened, usually via a mutation. Once the mutation publishes an event, the subscription server will inspect the published data and, assuming the data type matches the expected data for the subscription, it will execute the subscription method for any connected subscribers and deliver the results as necessary.
8489

85-
8690
```C#
8791
public class MutationController : GraphController
8892
{
@@ -106,6 +110,7 @@ public class MutationController : GraphController
106110
> Notice that the event name used in `PublishSubscriptionEvent` is the same as the `EventName` property on the `[SubscriptionRoot]` attribute. The subscription server will use the published event name to match which registered subscriptions need to receive the data being published.
107111
108112
### Subscription Event Source Data
113+
109114
In the example above, the data sent with `PublishSubscriptionEvent` is the same as the first input parameter called `eventData` which is the same as the field return type of the controller method. By default, the subscription will look for a parameter with the same data type as its field return type and use that as the event data source.
110115

111116
You can explicitly flag a different parameter, or a parameter of a different data type to be the expected event source with the `[SubscriptionSource]` attribute.
@@ -127,11 +132,13 @@ public class SubscriptionController : GraphController
127132
}
128133
}
129134
```
130-
Here the subscription expects that an event is published using a `WidgetInternal` data type that it will internally convert to a `Widget` and send to any subscribers. This can be useful if you wish to share internal objects between your mutations and subscriptions that you don't want publicly exposed.
131135

136+
Here the subscription expects that an event is published using a `WidgetInternal` data type that it will internally convert to a `Widget` and send to any subscribers. This can be useful if you wish to share internal objects between your mutations and subscriptions that you don't want publicly exposed.
132137

133138
### Summary
139+
134140
That's all there is for a basic subscription server setup.
141+
135142
1. Add the package reference and update startup.cs
136143
2. Create a new subscription using `[Subscription]` or `[SubscriptionRoot]`
137144
3. Publish an event from a mutation
@@ -141,16 +148,19 @@ That's all there is for a basic subscription server setup.
141148
A complete example of single instance subscription server including a react app that utilizes the Apollo Client is available in the [demo projects](../reference/demo-projects) section.
142149

143150
## Scaling Subscription Servers
151+
144152
Using web sockets has a natural limitation in that any each server instance has a maximum number of socket connections that it can handle. Once that limit is reached no additional clients can register subscriptions.
145153

146-
Ok no problem, just scale horizontally, spin up additional ASP.NET server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances, right? Not so fast.
154+
Ok no problem, just scale horizontally, spin up additional ASP.NET server instances, add a load balancer and have the new requests open a web socket connection to these additional server instances, right? Not so fast.
147155

148156
With the examples above events published by any mutation using `PublishSubscriptionEvent` are routed internally directly to the local subscription server meaning only those clients connected to the server where the event was raised will receive it. Clients connected to other server instances will never know an event was raised. This represents a big problem for large scale websites, so what do we do?
149157

150158
### Custom Event Publishing
151-
Instead of publishing events internally, within the server instance, we need to publish our events to some intermediate source such that any server can be notified of the change. There are a variety of technologies to handle this scenario; be it a common database or messaging technologies like RabbitMQ, Azure Service Bus etc.
159+
160+
Instead of publishing events internally, within the server instance, we need to publish our events to some intermediate source such that any server can be notified of the change. There are a variety of technologies to handle this scenario; be it a common database or messaging technologies like RabbitMQ, Azure Service Bus etc.
152161

153162
#### Implement `ISubscriptionEventPublisher`
163+
154164
Whatever your technology of choice the first step is to create and register a custom publisher. How your individual class functions will vary widely depending on your implementation.
155165

156166
```C#
@@ -166,6 +176,7 @@ Whatever your technology of choice the first step is to create and register a cu
166176
```
167177

168178
Register your publisher with the DI container BEFORE calling `.AddGraphQL()`
179+
169180
```C#
170181
// startup.cs
171182
@@ -185,7 +196,8 @@ Register your publisher with the DI container BEFORE calling `.AddGraphQL()`
185196
Publishing your SubscriptionEvents externally is not trivial. You'll have to deal with concerns like data serialization, package size etc..
186197

187198
### Consuming Published Events
188-
At this point, we've successfully published our events to some external data source. Now we need to consume them. How that occurs is, again, implementation specific. Perhaps you run a background hosted service to watch for messages on an Azure Service Bus topic or perhaps you periodically pole a database table to look for new events. The ways in which data may be shared is endless.
199+
200+
At this point, we've successfully published our events to some external data source. Now we need to consume them. How that occurs is, again, implementation specific. Perhaps you run a background hosted service to watch for messages on an Azure Service Bus topic or perhaps you periodically pole a database table to look for new events. The ways in which data may be shared is endless.
189201

190202
Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that it occurred. this is done using the `ISubscriptionEventRouter`. In general, you won't need to implement your own router, just inject it into your listener service then call `RaiseEvent` and GraphQL will take it from there.
191203

@@ -211,17 +223,18 @@ Once you rematerialize a `SubscriptionEvent` you need to let GraphQL know that i
211223
}
212224
}
213225
```
226+
214227
The router will take care of figuring out which schema the event is destined for, which local subscription servers are registered to receive that event and forward the data as necessary for processing.
215228

216229
### Azure Service Bus Example
217230

218231
A complete example of a scalable subscription configuration including serialization and deserialization using the Azure Service Bus is available in the [demo projects](../reference/demo-projects) section.
219232

220-
221233
## Subscription Server Configuration
222-
>See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.
223234

224-
Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:
235+
> See [schema configuration](../reference/schema-configuration#subscription-server-options) for information on individual subscription server configuration options.
236+
237+
Currently, when using the `.AddSubscriptions()` extension method two seperate operations occur:
225238

226239
1. The subscription server components are registered to the DI container, the graphql execution pipeline is modified to support registering subscriptions and a middleware component is appended to the ASP.NET pipeline to intercept web sockets and forward client connections to the the subscription server component.
227240

@@ -231,19 +244,29 @@ Some applications may wish to split these operations in different server instanc
231244

232245
The following more granular configuration options are available:
233246

234-
* `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.
235-
236-
* `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.
247+
- `.AddSubscriptionServer()` :: Only configures the ASP.NET pipeline to intercept websockets and adds the subscription server components to the DI container.
237248

249+
- `.AddSubscriptionPublishing()` :: Only configures the graphql execution pipeline and the `ISubscriptionEventPublisher`. Subscription registration and Websocket support is **NOT** enabled.
238250

239251
## Security & Query Authorization
240252

241253
Because subscriptions are long running and registered before any data is processed, the subscription server requires a [query authorization method](../reference/schema-configuration#authorization-options) of `PerRequest`. This allows the subscription query to be fully validated before its registered with the server. This authorization method is set globally at startup and will apply to queries and mutations as well.
242254

243-
This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions.
255+
This is different than the default behavior when subscriptions are not enabled. Queries and mutations, by default, will follow a `PerField` method allowing for partial query resolutions.
244256

245257
**Note:** Allowing `PerField` authorization for subscriptions is slated for a future release.
246258

259+
## Query Timeouts
247260

261+
By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. This is true for subscriptions as well. Given that the websocket connection is never closed while the end user is connected, any query executed through the websocket will be allowed to run for an infinite amount of time which can have some unintended side effects and consume resources unecessarily.
248262

263+
Optionally, you can define a query timeout for a given schema, which the subscription server will obey:
249264

265+
```csharp
266+
// startup.cs
267+
services.AddGraphQL(o =>
268+
{
269+
// define a 2 minute timeout per query executed.
270+
o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);
271+
})
272+
```

docs/controllers/actions.md

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ public class BakeryController : GraphController
516516
// schema syntax: [Donut]
517517
[Mutation("donutsAsAnArray")]
518518
public bool DonutsAsAnArray(Donut[] donuts)
519-
{/*....*/}
519+
{/*....*/}
520520

521521
// This is a valid nested list
522522
// schema syntax: [[[Donut]]]
@@ -562,7 +562,7 @@ public class BakeryController : GraphController
562562

563563
```javascript
564564
query {
565-
searchDonuts(searchParams:
565+
searchDonuts(searchParams:
566566
name: "jelly*"
567567
filled: true
568568
dayOld: false){
@@ -580,7 +580,7 @@ At runtime, GraphQL will try to validate every parameter passed on a query again
580580

581581
One might think, well it should be passed as an object reference to the dictionary parameter:
582582

583-
```javascript
583+
```ruby
584584
query {
585585
searchDonuts( searchParams : {name: "jelly*" filled: true dayOld: false }){
586586
id
@@ -608,8 +608,6 @@ public class DonutSearchParams
608608
// BakeryController.cs
609609
public class BakeryController : GraphController
610610
{
611-
// ERROR, a GraphDeclarationException
612-
// will be thrown.
613611
[QueryRoot]
614612
public IEnumerable<Donut>
615613
SearchDonuts(DonutSearchParams searchParams)
@@ -626,3 +624,41 @@ query {
626624
```
627625

628626
</div>
627+
628+
## Cancellation Tokens
629+
630+
As with REST based ASP.NET action methods, your graph controller action methods can accept an optional cancellation token. This is useful when doing some long running activities such as IO, database queries, API orchestration etc. To make use of a cancellation token simply add it as a parameter to your method. GraphQL will automatically wire up the token for you:
631+
632+
```csharp
633+
// BakeryController.cs
634+
public class BakeryController : GraphController
635+
{
636+
// Add a CancellationToken to your controller method
637+
[QueryRoot(typeof(IEnumerable<Donut>))]
638+
public async Task<IGraphActionResult> SearchDonuts(string name, CancellationToken cancelToken)
639+
{/* ... */}
640+
}
641+
```
642+
643+
> Depending on your usage of the cancellation token a `TaskCanceledException` may be thrown. GraphQL will not attempt to intercept this exception and will log it as an error-level, unhandled exception if allowed to propegate. The query will still be cancelled as expected.
644+
645+
### Defining a Query Timeout
646+
647+
By default GraphQL does not define a timeout for an executed query. The query will run as long as the underlying HTTP connection is open. In fact, the `CancellationToken` passed to your action methods is the same Cancellation Token offered on the HttpContext when it receives the initial post request.
648+
649+
Optionally, you can define a query timeout for a given schema:
650+
651+
```csharp
652+
// startup.cs
653+
services.AddGraphQL(o =>
654+
{
655+
// define a 2 minute timeout per query executed.
656+
o.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);
657+
})
658+
```
659+
660+
When a timeout is defined, the token passed to your action methods is a combined token representing the HttpContext as well as the timeout operation. That is to say the token will indicate a cancellation if the alloted query time expires or the http connection is closed which ever comes first. When the timeout expires the caller will receive a response indicating the timeout. However, if the its the HTTP connection that is closed, the operation is simply halted and no result is produced.
661+
662+
### Timeouts and Subscriptions
663+
664+
The same rules for cancellation tokens apply to subscriptions as well. Since the websocket connection is a long running operation it will never be closed until the connection is closed. To prevent some processes from spinning out of control its a good idea to define a query timeout when implementing a subscription server. This way, even though the connection remains open the query will terminate and release resources if something goes awry.

docs/reference/schema-configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ schemaOptions.ExecutionOptions.QueryTimeout = TimeSpan.FromMinutes(2);
175175

176176
| Default Value | Acceptable Values |
177177
| ------------- | -------------------------- |
178-
| 1 Minute | Minimum of 10 milliseconds |
178+
| null | Minimum of 10 milliseconds |
179179

180-
The amount of time an individual query will be given to run to completion before being abandoned and canceled by the runtime.
180+
The amount of time an individual query will be given to run before being abandoned and canceled by the runtime. By default, the timeout is disabled and a query will continue to execute as long as the underlying HTTP request is also executing.
181181

182182
### DebugMode
183183

website/pages/en/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class HomeSplash extends React.Component {
3131
<h2 className="projectTitle">
3232
{<span>GraphQL ASP.NET</span>}
3333
{/*<small>{siteConfig.tagline}</small>*/}
34-
<small>v0.12.0-beta</small>
34+
<small>v0.12.1-beta</small>
3535
</h2>
3636
);
3737

0 commit comments

Comments
 (0)