Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ Core TODOs are required to finish; Stretch TODOs are optional extras for learner
who finish early, and nothing later depends on them.
_Avoid_: starter, skeleton, template

**Stretch Workshop**:
A Workshop whose Track is offered, in every Dev Day, only to learners who
finish early. Everyone still sees its Deck segment; nothing later in a Dev Day
depends on its Track. It describes live delivery only: on its own, the
Workshop and its Track are like any other.
_Avoid_: bonus lab, optional workshop, extra

**Solution**:
The completed counterpart of an Exercise, structurally identical to it.
_Avoid_: answer, final, complete
Expand Down
112 changes: 112 additions & 0 deletions decks/java-master/components/ContinueAsNewRuns.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!--
ABOUTME: One Workflow ID as a chain of runs: each run's history fills, it continues as new,
ABOUTME: and the next run starts with an empty history and only the state it was handed.

Frontmatter usage (click 1 adds run 2, click 2 adds run 3):
---
clicks: 2
---

<ContinueAsNewRuns />
-->
<script setup lang="ts">
import { useSlideContext } from '@slidev/client'

const { $clicks } = useSlideContext()

const RUNS = [
{ name: 'Run 1', runId: 'Run ID 7c2e…', fill: 92 },
{ name: 'Run 2', runId: 'Run ID a41f…', fill: 92 },
{ name: 'Run 3', runId: 'Run ID e90b…', fill: 18 },
]
const HANDOFFS = ['cyclesCompleted: 41', 'cyclesCompleted: 83']

// A run is closed once the next one is on screen.
const closed = (i: number) => $clicks.value > i
</script>

<template>
<figure class="can" aria-label="One Workflow ID, three runs. Each run's history fills up, it continues as new and passes cyclesCompleted to the next run, which starts with an empty history.">
<div class="wid">
<span class="label">Workflow ID</span>
<code>subscription-42</code>
<span class="hint">what callers see: one Workflow</span>
</div>

<div class="row">
<template v-for="(run, i) in RUNS" :key="run.name">
<div v-if="i > 0" class="handoff" :class="{ hidden: $clicks < i }">
<code class="call">continueAsNew</code>
<div class="arrow" />
<span class="state">{{ HANDOFFS[i - 1] }}</span>
</div>

<div class="run" :class="{ hidden: $clicks < i, done: closed(i) }">
<div class="head">
<strong>{{ run.name }}</strong>
<code>{{ run.runId }}</code>
</div>
<div class="hist-label">
<span>History</span>
<span v-if="run.fill > 50" class="suggested">near the limit</span>
<span v-else>fresh</span>
</div>
<div class="bar"><div class="fill" :style="{ width: `${run.fill}%` }" /></div>
<div class="status">{{ closed(i) ? 'Continued-As-New' : 'Running' }}</div>
</div>
</template>
</div>
</figure>
</template>

<style scoped>
.can { margin: 1.5rem 0 0; display: flex; flex-direction: column; gap: 1.2rem; }

.wid {
display: flex; align-items: baseline; gap: 0.8rem; padding: 0.5rem 0.9rem;
border: 1px dashed var(--temporal-border-hi); border-radius: 0.6rem;
}
.wid .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--temporal-text-muted); }
.wid code { font-size: 1rem !important; color: var(--temporal-text-strong); }
.wid .hint { margin-left: auto; font-size: 0.8rem; color: var(--temporal-text-muted); }

.row { display: flex; align-items: stretch; gap: 0.6rem; }

.run {
flex: 1 1 0; min-width: 0; padding: 0.8rem 0.9rem; border-radius: 0.6rem;
border: 1px solid var(--temporal-green); background: var(--temporal-bg-elev);
display: flex; flex-direction: column; gap: 0.45rem;
transition: opacity 0.35s ease;
}
.run.done { border-color: var(--temporal-border); }
.head { display: flex; flex-direction: column; gap: 0.1rem; white-space: nowrap; }
.head strong { color: var(--temporal-text-strong); }
.head code { font-size: 0.75rem !important; color: var(--temporal-text-muted); }

.hist-label { display: flex; justify-content: space-between; gap: 0.5rem; white-space: nowrap; font-size: 0.75rem; color: var(--temporal-text-muted); }
.suggested { color: var(--temporal-yellow); }
.bar { height: 0.7rem; border-radius: 999px; background: var(--temporal-border); overflow: hidden; }
.fill { height: 100%; border-radius: 999px; background: var(--temporal-cyan); }
.run .fill { transition: background 0.35s ease; }
.run:not(:last-child) .fill { background: var(--temporal-yellow); }

.status { font-size: 0.8rem; color: var(--temporal-green); }
.run.done .status { color: var(--temporal-text-muted); }

.handoff {
flex: 0 0 10rem; white-space: nowrap; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 0.35rem; transition: opacity 0.35s ease;
}
.call { font-size: 0.75rem !important; color: var(--temporal-lavender); }
.arrow { position: relative; width: 100%; height: 2px; background: var(--temporal-lavender); }
.arrow::after {
content: ''; position: absolute; right: -2px; top: -5px;
border-left: 9px solid var(--temporal-lavender); border-top: 6px solid transparent; border-bottom: 6px solid transparent;
}
.state {
font-family: var(--slidev-code-font-family, monospace); font-size: 0.7rem; padding: 0.1rem 0.5rem;
border-radius: 999px; border: 1px solid var(--temporal-lavender); color: var(--temporal-text-strong);
}

.hidden { opacity: 0; }
</style>
4 changes: 2 additions & 2 deletions decks/java-master/segments/14-child-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ closes, and only the child's lane changes.
one people misunderstand: the child keeps running even though the parent is
gone. That is intentional, and powerful for fire-and-forget sub-processes.
- **Click 3 - `REQUEST_CANCEL`.** The child is sent a cancellation request and
can clean up gracefully, the same cancellation the Saga section uses to run
compensations, before it ends as cancelled.
can clean up gracefully before it ends as cancelled: the cancellation the
Saga section pointed to, where compensations still run in a detached scope.

The policy applies whenever the parent closes, whether it completed, failed,
timed out or was terminated. Not only on failure. In code these are
Expand Down
72 changes: 72 additions & 0 deletions decks/java-master/segments/15-continue-as-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ from the Workflow ID alone follows the chain: `WorkflowStub.getResult(...)`
waits for the last run.
-->

---
layout: default
clicks: 2
---

# One Workflow, many runs

<ContinueAsNewRuns />

<!--
The previous slide as a picture: the subscription from the walkthrough coming
up, a few years in.

- **Start.** Run 1 bills month after month. Its Event History fills until
`isContinueAsNewSuggested()` turns true.
- **Click 1.** It calls `continueAsNew` with only `cyclesCompleted`. Run 2
starts under the same Workflow ID with a new Run ID and an empty history,
and picks up at cycle 42.
- **Click 2.** Same again. Run 3 is the one running now; runs 1 and 2 are
closed as Continued-As-New, and their histories are still there in the UI.

Point at the dashed box: callers only ever use `subscription-42`. A Signal or
`getResult` by Workflow ID reaches whichever run is current.
-->

---
layout: default
---
Expand Down Expand Up @@ -89,6 +114,53 @@ Worker Versioning, a Pinned run's continuation inherits its version unless you
ask for `AUTO_UPGRADE`.)
-->

---
layout: center
class: text-center
---

<p style="font-size: 2.6rem; font-weight: 200; line-height: 1.2">Why doesn't Temporal default to Continue&#8209;As&#8209;New?</p>

<p v-click style="font-size: 2.2rem; font-weight: 300; margin-top: 2rem; color: var(--temporal-green)">Only your code knows what state to carry forward.</p>

<div v-click style="font-size: 1.2rem; margin-top: 2rem; line-height: 1.8">

History is the state · It needs cleanup first · Most Workflows never need it

</div>

<!--
Ask it, let the room answer, then click. Someone usually says "performance",
and the second click is why that is not the reason.

- **History is the state.** Temporal does not snapshot your Workflow's
variables; it rebuilds them by replaying the Event History. Cut the history
and the state goes with it, unless something carries it across. That
something is the input to the new run, and only your code knows what belongs
in it: `cyclesCompleted` yes, the loop's scratch variables no.
- **It needs cleanup first.** In-flight work does not come along, so the
current run has to wind down before it hands over, and only your code knows
what that takes:
- let running Signal and Update handlers finish:
`Workflow.await(() -> Workflow.isEveryHandlerFinished())`;
- drain Signals buffered in a field (a queue of pending changes) by
processing them or passing them in the new run's input, or they are lost;
- wait for or cancel pending Activities, and decide what happens to running
Child Workflows (their Parent Close Policy);
- release anything held outside Temporal, such as a lease or a reservation,
through an Activity.

Then continue at a point between steps (after a charge, before the next
sleep). The Server cannot tell when the run is in that state.
- **Most Workflows never need it.** An order, a transfer, a Saga finishes in
hundreds of Events, far below the limits. One unbroken history is also the
better audit trail and debugging record, and a new run means a new Run ID
that anyone holding the old one has to follow.

So Temporal does the part it can see, and tells you when it is time:
`isContinueAsNewSuggested`, on the next slide but one.
-->

---
layout: code-walkthrough
heading: "Continue-As-New in practice: a monthly subscription"
Expand Down
111 changes: 61 additions & 50 deletions decks/java-master/slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ themeConfig:
label: Updates
- id: quiz
label: Quiz
- id: saga
label: "Compensations: the Saga pattern"
- id: child
label: Child Workflows
- id: can
label: Continue-As-New
- id: heartbeats
label: Heartbeats
- id: saga
label: "Compensations: the Saga pattern"
- id: patterns
label: Patterns and anti-patterns
- id: claim-check
Expand Down Expand Up @@ -198,7 +198,7 @@ layout: section

# Advanced Temporal Topics

Child Workflows, Continue‑As‑New, Heartbeats, Sagas, and beyond.
Sagas, Child Workflows, Continue‑As‑New, Heartbeats, and beyond.

</ZiggyHero>

Expand All @@ -214,52 +214,6 @@ post); there is no official wizard version, so check
with brand before using it outside a workshop.
-->

---
src: ./segments/14-child-workflows.md
---

---
src: ./segments/15-continue-as-new.md
---

---
src: ./segments/16-heartbeating.md
---

---
layout: exercise
minutes: 35
heading: Labs
---

<CopyLink href="https://play.instruqt.com/temporal/invite/zxfg8rb7ygov" />

- Child Workflows · 15 min
- Continue-As-New · 10 min
- Heartbeats · 10 min

<!--
The labs for the three sections just taught, in this order. Each is its own
Track, and all of them sit behind the one invite link; the minutes are each
lab's measured length, and each Track's time limit is 30.

- **Child Workflows** (workshops/child-workflows): run one child, chain
children five deep and follow a failure up from the bottom, fan out and watch
the default Parent Close Policy terminate every child, then write the fan in
with Async.function and Promise.allOf.
- **Continue-As-New** (workshops/continue-as-new-subscription): a 30-cycle
subscription is terminated by the Service at its history limit, then rolls
over on isContinueAsNewSuggested() and finishes. The lab's dev server lowers the
limits to 40 and 200 Events so both happen in seconds.
- **Heartbeats** (workshops/heartbeats-shipment): crash a Worker ten items into
a packing job and watch the retry wait out the start-to-close timeout and
start again at item 0; then add setHeartbeatTimeout, heartbeat(packed) and a
resume from getHeartbeatDetails, crash it again, and watch the retry start in
seconds where it left off.

Leave this slide up while people open the labs.
-->

---
layout: section
---
Expand Down Expand Up @@ -309,20 +263,77 @@ src: ./segments/17-saga-07-related.md

---
layout: exercise
minutes: 25
minutes: 30
heading: "Lab: The Saga Pattern"
---

<CopyLink href="https://play.instruqt.com/temporal/invite/zxfg8rb7ygov" />

Finished early? **Heartbeats** · 10 min

<!--
The Saga Workshop's lab (workshops/saga-order-fulfillment):
an order that reserves stock, charges a card and dispatches a courier, and
undoes the first two when the third fails.

The Saga gets the most lab time of the day because learners rate it the most
useful. Thirty minutes covers its four Challenges; the Track's own limit is
40. Heartbeats is the Stretch Workshop (ADR 0005): anyone who finishes early
can open it from the same invite link. Nothing later depends on it.

Leave this slide up while people open the lab.
-->

---
src: ./segments/14-child-workflows.md
---

---
src: ./segments/15-continue-as-new.md
---

---
src: ./segments/16-heartbeating.md
---

---
layout: exercise
minutes: 25
heading: Labs
---

<CopyLink href="https://play.instruqt.com/temporal/invite/zxfg8rb7ygov" />

- Child Workflows · 15 min
- Continue-As-New · 10 min

Finished early? **Heartbeats** · 10 min

<!--
The labs for the three sections just taught. Child Workflows and
Continue-As-New are for everyone, in that order. Heartbeats is the Stretch
Workshop (ADR 0005): only for learners who finish early, and nothing later
depends on it. The slides taught it to everyone; only the lab is optional.
Each is its own Track, and all of them sit behind the one invite link; the
minutes are each lab's measured length, and each Track's time limit is 30.

- **Child Workflows** (workshops/child-workflows): run one child, chain
children five deep and follow a failure up from the bottom, fan out and watch
the default Parent Close Policy terminate every child, then write the fan in
with Async.function and Promise.allOf.
- **Continue-As-New** (workshops/continue-as-new-subscription): a 30-cycle
subscription is terminated by the Service at its history limit, then rolls
over on isContinueAsNewSuggested() and finishes. The lab's dev server lowers the
limits to 40 and 200 Events so both happen in seconds.
- **Heartbeats** (workshops/heartbeats-shipment): crash a Worker ten items into
a packing job and watch the retry wait out the start-to-close timeout and
start again at item 0; then add setHeartbeatTimeout, heartbeat(packed) and a
resume from getHeartbeatDetails, crash it again, and watch the retry start in
seconds where it left off.

Leave this slide up while people open the labs.
-->

---
src: ./segments/17b-patterns-and-anti-patterns.md
---
Expand Down
Loading
Loading