Skip to content

Commit 6b52b5f

Browse files
docs(sub): polish substantial docs (#967)
#### Migration notes None - [ ] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [x] End-user documentation is updated to reflect the change <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Enhanced documentation for Substantial runtime. - Added new section on Backend operations. - Renamed existing Backend section to "Persistence and Lifecycle." - Introduced new subsections on workflow management concepts including Context, Interrupts, Save, Send/Receive, and Ensure. - Added a new section on Advanced Filters with examples for Python and TypeScript. - Corrected typographical errors for improved clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: michael-0acf4 <[email protected]>
1 parent a9e0336 commit 6b52b5f

File tree

1 file changed

+221
-5
lines changed
  • docs/metatype.dev/docs/reference/runtimes/substantial

1 file changed

+221
-5
lines changed

docs/metatype.dev/docs/reference/runtimes/substantial/index.mdx

Lines changed: 221 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import TabItem from "@theme/TabItem";
55

66
## Substantial runtime
77

8-
The Substantial runtime enables the execution of durable workflows in one or accross multiple typegates.
8+
The Substantial runtime enables the execution of durable workflows in one or across multiple typegates.
99

1010
Why use it?
1111

1212
- **Long-running "processes"**: Durable tasks that need to run over extended periods (days, weeks or months), handling **retries** and **restarts** seamlessly.
1313
- **Fault-tolerant execution**: Ensure reliable execution of tasks, even upon failures, by maintaining a durable state of the latest run.
14-
- **Task orchestration**: Coordinate complex sequences of workflows (analogous to microservices interactions).
14+
- **Task orchestration**: Coordinate complex sequences of workflows (analogous to microservice interactions).
1515

16-
For example, the workflow bellow will continue running until a `confirmation` event is sent to the **associated run**.
16+
For example, the workflow below will continue running until a `confirmation` event is sent to the **associated run**.
1717

1818
```typescript
1919
export async function sendEmail(ctx: Context) {
@@ -37,20 +37,233 @@ Additionally, if we were to shut down the Typegate node executing it and then re
3737

3838
## Key Concepts
3939

40+
### Backend
41+
42+
This abstraction implements a set of atomic operations that allows Typegate to persist and recover the workflow state. Currently, we have the **Redis** backend available, along with others like **fs** and **memory**, which are primarily intended for development or testing purposes.
43+
4044
### Workflows
4145

4246
A special type of function with **durable state** and an execution mechanism directly tied to time. A workflow can also trigger other workflows (child workflows).
4347

44-
### Backend
48+
#### Persistence and Lifecycle
4549

46-
This abstraction implements a set of atomic operations that allows Typegate to persist and recover the workflow state. Currently, we have the **Redis** backend available, along with others like **fs** and **memory**, which are primarily intended for development or testing purposes.
50+
- **Context**
51+
52+
The context object contains the workflow input (namely `kwargs` as seen in the example above), but it can also be thought as a namespace that contains all of the core functions used for durableness.
53+
54+
It is recreated at every replay.
55+
56+
- **Interrupts**
57+
58+
A special state of the program that is produced by any function that can trigger a workflow **replay**.
59+
60+
An interrupt will pause the program at the line it was emitted then queue it back to execute later.
61+
62+
One simple example of such function is when you want to wait for a given amount of time, Substantial will save the current time and the end time, interrupts the workflow then requeue it to execute later.
63+
64+
Any agent (Typegate node) that picks the workflow, will **replay** it, the cycle repeats until the actual current time is greater or equal to the end time.
65+
66+
```typescript
67+
await ctx.sleep(24 * 3600 * 1000); // 1 day
68+
```
69+
70+
- **Save**
71+
72+
A save is one of the main building blocks of a workflow, many functions available on the context object rely on it.
73+
74+
This is mainly because a save call converts any function into a **durable** one: the function output is saved and persisted in the backend. On subsequent executions, the saved value is retrieved from the backend instead of re-executing the function.
75+
76+
This ensures that when a workflow is resumed (after a Typegate reboot for example) or replayed (after interrupts), the saved function will not be executed again.
77+
78+
```typescript
79+
// For example, if the output was 7 then after replay,
80+
// save will not execute the function inside but directly return already persisted value, which was 7.
81+
const rand = await ctx.save(() => Math.floor(10 * Math.random()));
82+
83+
// If you keep in mind that the workflow can be replayed many times
84+
// A save call should make more sense!
85+
const now = await ctx.save(() => Date.now());
86+
87+
// And even more for functions that can produce external side-effects
88+
const result = await ctx.save(() => sendEmail());
89+
```
90+
91+
:::info Notes
92+
93+
- Only JSON-compliant values can be persisted. The execution will throw otherwise.
94+
- Make sure to not rely on changing outside references inside a save call, best is to always expect a replay.
95+
96+
```typescript
97+
let value = 5;
98+
const afterSave = await save(() => {
99+
value *= 2;
100+
return save; // 10 will be stored on the Backend
101+
});
102+
103+
console.log(value); // 10 on the first replay, 5 on the next replay (save call function was skipped)
104+
console.log(afterSave); // always 10
105+
106+
// Ideally, what you want is to reuse the saved value if the effect was desired
107+
// especially when you branch
108+
if (afterSave == 10) {
109+
console.log("All good"); // branch path is now durable even after replays!
110+
} else {
111+
throw new Error("Unreachable code");
112+
}
113+
```
114+
115+
:::
116+
117+
- **Send/Receive**
118+
119+
You can send events to a workflow through GraphQL, any receive call on the workflow will await for it and will **interrupt** the workflow if it hasn't been received yet.
120+
121+
<SDKTabs>
122+
<TabItem value="python">
123+
124+
```python
125+
g.expose(
126+
# ..
127+
send=sub.send(t.integer())
128+
)
129+
```
130+
131+
</TabItem>
132+
<TabItem value="typescript">
133+
134+
```typescript
135+
g.expose({
136+
// ..
137+
send: sub.send(t.integer()),
138+
});
139+
```
140+
141+
</TabItem>
142+
143+
</SDKTabs>
144+
145+
```graphql
146+
# Client
147+
query {
148+
send(run_id: "<workflow_run_id>", event: { name: "myEvent", payload: 1234 })
149+
}
150+
```
151+
152+
```typescript
153+
// Workflow
154+
const value = ctx.receive<number>("myEvent"); // 1234
155+
```
156+
157+
- **Ensure**
158+
159+
It's a function that takes a predicate, and will **interrupt** the workflow so that it will be replayed later if the returned value is false.
160+
161+
```typescript
162+
const secret = ctx.receive<string>("secret");
163+
await ctx.ensure(() => secret == "top_secret");
164+
//
165+
// continue execution
166+
```
47167

48168
### Run
49169

50170
When a workflow is started, a run is created and Substantial will provide you a `run_id` to uniquely identify it.
51171

52172
You can send an event or abort an ongoing run from its `run_id`.
53173

174+
### Advanced Filters
175+
176+
In practice, you will have many workflows that have been executed, each run can be in different states. Substantial provides a way to filter the runs.
177+
178+
<SDKTabs>
179+
<TabItem value="python">
180+
181+
```python
182+
g.expose(
183+
# ..
184+
search=sub.advanced_filters()
185+
)
186+
```
187+
188+
</TabItem>
189+
<TabItem value="typescript">
190+
191+
```typescript
192+
g.expose({
193+
// ..
194+
search: sub.advancedFilters(),
195+
});
196+
```
197+
198+
</TabItem>
199+
200+
</SDKTabs>
201+
202+
```graphql
203+
# Client
204+
query {
205+
search(
206+
name: "sendEmail"
207+
filter: {
208+
and: [
209+
{ status: { contains: "\"COMPLETED\"" } }
210+
{ not: { started_at: { lt: "\"2025-01-15T00:00:00Z\"" } } }
211+
{ not: { eq: "null" } }
212+
]
213+
}
214+
) {
215+
run_id
216+
started_at
217+
ended_at
218+
status
219+
value
220+
}
221+
}
222+
```
223+
224+
- **Specification and syntax**
225+
226+
The specification itself is very close to Prisma queries.
227+
You can also refer to your GraphQL playground for guiding you into expressing your query.
228+
229+
Base specification:
230+
231+
```
232+
val ::= json_string
233+
234+
term ::= { eq: val }
235+
| { lt: val } | { lte: val }
236+
| { gt: val } | { gte: val }
237+
| { in: val } | { contains: val }
238+
239+
special_term ::= { started_at: term }
240+
| { ended_at: term }
241+
| { status: term }
242+
243+
not ::= { not: expr }
244+
or ::= { or: [expr] }
245+
and ::= { and: [expr] }
246+
247+
expr ::= not | or | and | term | special_term
248+
```
249+
250+
:::info Notes
251+
252+
- **contains**: Check if the workflow output is a list that contains the given value or if the given value is a substring of it.
253+
- **in**: Check if the workflow output is within a list or is a substring of the given value.
254+
- **status**
255+
- Can be one of `"COMPLETED"`, `"COMPLETED_WITH_ERROR"`, `"ONGOING"` or `"UNKNOWN"`.
256+
257+
For example, the term
258+
259+
```graphql
260+
{ status: { contains: "\"COMPLETED\"" } }
261+
```
262+
263+
..should cover `"COMPLETED"` and `"COMPLETED_WITH_ERROR"`.
264+
265+
::::
266+
54267
## Child workflows
55268

56269
Child workflows are like any other workflows, they are just run by another workflow (parent).
@@ -176,6 +389,9 @@ def substantial_example(g: Graph):
176389
send_single_email=sub.start(t.struct({"to": t.email()})).reduce(
177390
{"name": "sendEmail"}
178391
),
392+
send_confirmation=sub.send(t.boolean()).reduce(
393+
{"event": {"name": "confirmation", "payload": g.inherit()}}
394+
),
179395
results_raw=sub.query_results_raw(),
180396
workers=sub.query_resources(),
181397
**sub.internals(), # Required for child workflows

0 commit comments

Comments
 (0)