As mentioned in the "Execution scheduling and ordering" section of the specification, UPWARD servers must run their resolvers:
- only when their results are used
- as concurrently as possible
One approach to this is a state machine. A state machine can provide lazy execution by traversing a graph while keeping track of outstanding dependencies, and it can manage concurrency by spawning and keeping references to other instances of the same state machine.
Consider the following upward.yml
configuration:
status: page.status
headers: page.headers
body: page.body
articleResult:
url: env.LIBRARY_SVC
query: './getArticle.graphql'
variables:
articleId: request.url.query.artID
authorBioResult:
url: env.LIBRARY_SVC
query: './getAuthor.graphql'
variables:
searchTerm: request.url.query.authorID
textHtml:
inline:
'content-type': 'text/html'
notFound:
inline:
status: 404
headers: textHtml
body:
engine: mustache
template: './notFound.mst'
page:
when:
- matches: request.url.pathname
pattern: '/article'
use:
when:
- matches: articleResult.data.article.id
pattern: '.'
use:
inline:
status: 200
headers: textHtml
body:
engine: mustache
template: './article.mst'
default: notFound
- matches: request.url.pathname
pattern: '/author'
use:
when:
- matches: authorBioResult.data.author.id
pattern: '.'
use:
inline:
status: 200
headers: textHtml
body:
engine: mustache
template: './authorBio.mst'
default: notFound
default: notFound
I/O dependent tasks defined by resolvers in this configuration include:
A. Reading and parsing ./getArticle.graphql
from the file system
B. Querying a remote service for articles
C. Reading and parsing ./getAuthor.graphql
from the file system
D. Querying a remote service for authors
E. Reading and parsing ./authorBio.mst
from the file system
F. Reading and parsing ./article.mst
from the file system
G. Reading and parsing ./notFound.mst
from the file system
In the above example, a request to /author?id=1
should never cause the articleResult
resolver to execute its query to the remote service. If the result is not found, it should never cause the authorBio.mst
resolver to read from the file system. Even though the file format is declarative, each request should traverse through the resolvers using a minimal path. The request /author?id=1
, if no data exists for it in the backing resource, should only run tasks C, D, and G from the above list.
ℹ️ (The below is an example and not normative. UPWARD-compliant servers must lazily execute resolvers, and a state machine is one way to implement this. What follows is a description of one path through such a state machine, not a formal definition of a state machine.)
In a state-machine-based lazy-execution implementation, the request /author?id=1
would cause the following.
Starting state: Response readiness analysis
-
status
,headers
, andbody
have not been assigned. -
No resolvers are currently in operation or have resolved.
-
status
,headers
, andbody
resolution are all defined in configuration.State: Context dependency analysis
-
The context names pending resolution all depend on
page
. -
page
has not been assigned. -
page
resolution is defined in configuration.State: Context resolution
-
page
depends on a ConditionalResolver.State: Conditional execution
-
The first matcher depends on
request.url.pathname
. -
That value is already present in context, so the matcher can run.
State: Conditional matching
-
The first matcher tests
request.url.pathname
, which is the value/author
, against the pattern/article
. The test fails.State: Conditional execution
-
The second matcher depends on
request.url.pathname
. -
That value is already present in context, so the matcher can run.
State: Conditional matching
-
The second matcher tests
request.url.pathname
, which is the value/author
, against the pattern/author
. The test passes. -
The conditional yields to the resolver in the
use
parameter of the second matcher. That resolver is a ConditionalResolver.State: Conditional execution
-
The first matcher depends on
authorBioResult
. -
authorBioResult
has not been assigned.State: Resolver dependency detection
-
authorBioResult
resolution is defined in configuration.State: Context resolution
-
authorBioResult
depends on a ServiceResolver.State: Resolver dependency detection
-
The ServiceResolver depends on context values
env.LIBRARY_SVC
,request.url.query.authorId
, and a FileResolver (shorthand form) for thequery
. -
Both context values are present.
-
The shorthand FileResolver creates and resolves an implicit InlineResolver, determining that it depends on the filesystem path
./getAuthor.graphql
.State: File resolution
-
The server reads, parses, and caches the contents of
./getAuthor.graphql
.State: Service resolution
-
The server places an HTTP request to the GraphQL service. The GraphQL service responds with a result payload:
{ "data": { "author:" null } }
State: Context assignment
-
The context value
authorBioResult
is set to the result payload.State: Response readiness analysis
-
status
,headers
, andbody
have not been assigned. -
One or more context values have been assigned.
-
A ConditionalResolver is still resolving.
State: Context dependency analysis
-
The context names pending resolution depend on
page
andauthorBioResult
. -
page
has not been assigned. -
authorBioResult
has been assigned.State: Context resolution
-
The conditional resolver depending on
authorBioResult
resumes.State: Conditional execution
-
The first matcher runs again and fails again.
-
The second matcher runs again and succeeds again, yielding another ConditionalResolver.
-
The first matcher depends on
authorBioResult
. -
That value is now present in context, so the matcher can run.
State: Conditional matching
-
The first matcher tests
authorBioResult.data.author.id
against the pattern.
. BecauseauthorBioResult.data.author
is null, the match fails.State: Conditional execution
-
All matchers in the
when
configuration have failed, so the conditional yields to the value ofdefault
. -
The value of
default
is the context lookupnotFound
. -
notFound
has not been assigned.State: Resolver dependency detection
-
notFound
resolution is defined in configuration.State: Context resolution
-
notFound
depends on an InlineResolver. -
The InlineResolver specifies an object whose properties depend on context value
textHtml
forheaders
and a TemplateResolver forbody
. -
textHtml
has not been assigned. -
textHtml
is defined in configuration. -
textHtml
depends on an InlineResolver. -
The InlineResolver specifies an object whose properties depend on context value
text/html
. -
That value is present in context (it is built in).
-
The InlineResolver depending on
textHtml
resolves, assigning a value to theheader
property of thenotFound
resolver.State: Resolver dependency detection
-
The TemplateResolver depends on a context value
mustache
forengine
, and a shorthand FileResolver fortemplate
. -
The value
mustache
is present in context (it is built in). -
The shorthand FileResolver creates and resolves an implicit InlineResolver, determining that it depends on the filesystem path
./notFound.mst
.State: File resolution
-
The server reads, parses, and caches the contents of
./notFound.mst
.State: Template resolution
-
The TemplateResolver executes its template against the entire context object, creating an interpolated string:
<html><body>That doesn't look like anything to me.</body></html>
The TemplateResolver depending on
./notFound.mst
resolves, assigning the string to thebody
property of thenotFound
resolver.State: Context resolution
-
The InlineResolver for
notFound
resolves. -
The conditional resolver depending on
notFound
resumes.State: Conditional execution
-
The first matcher runs again and fails again.
-
All matchers in the
when
configuration have failed, so the conditional yields to the value ofdefault
. -
The value of
default
is a resolved InlineResolver producing an object.State: Conditional resolution
-
The ConditionalResolver resolves.
-
The
use
property of the match in the parent ConditionalResolver is fully resolved. -
The parent ConditionalResolver resolves.
State: Context assignment
-
The context value
page
is set to the result payload.State: Response readiness analysis
-
status
,headers
, andbody
have not been assigned. -
One or more top-level context values have resolved.
-
No resolvers are currently resolving.
State: Context dependency analysis
-
The context names pending resolution depend on
page
. -
page
has been assigned.State: Context resolution
-
status
,headers
, andbody
depend onpage
. -
page
exists in context and can be used.State: Context assignment
-
The context value
status
is set topage.status
. -
headers
is set topage.headers
. -
body
is set topage.body
.State: Response readiness analysis
-
status
,headers
, andbody
have all been assigned.State: Response flush
-
The context is cast to an HTTP response and sent to the requesting client.
While resolving the request, the state machine did not execute all possible tasks; it did not run tasks A, B, E, or F, even though there is no indication of task ordering or conditional execution in those task definitions themselves.
This topologically-ordered, maximally concurrent, and lazily evaluated process enables UPWARD configuration authors to describe the available tasks without writing any imperative logic.