Skip to content

Commit 3b6e271

Browse files
committed
feat: add ResettableTimer for simplified timer management
1 parent 7ce86d7 commit 3b6e271

File tree

3 files changed

+487
-0
lines changed

3 files changed

+487
-0
lines changed

x/resettabletimer/readme.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
### Resettable Timers in Cadence Workflows
2+
3+
#### Status
4+
5+
November 4, 2025
6+
7+
This is experimental and the API may change in future releases.
8+
9+
#### Background
10+
11+
In Cadence workflows, timers are a fundamental building block for implementing timeouts and delays. However, standard timers cannot be reset once created - you must cancel the old timer and create a new one, which can lead to complex code patterns.
12+
13+
The resettable timer provides a simple way to implement timeout patterns that need to restart based on external events.
14+
15+
#### Getting Started
16+
17+
Import the package:
18+
19+
```go
20+
import (
21+
"go.uber.org/cadence/workflow"
22+
"go.uber.org/cadence/x/resettabletimer"
23+
)
24+
```
25+
26+
#### Basic Usage
27+
28+
Create a timer that can be reset:
29+
30+
```go
31+
func MyWorkflow(ctx workflow.Context) error {
32+
// Create a timer that fires after 30 seconds
33+
timer := resettabletimer.New(ctx, 30*time.Second)
34+
35+
// Wait for the timer
36+
err := timer.Future.Get(ctx, nil)
37+
if err != nil {
38+
return err
39+
}
40+
41+
// Timer fired - handle timeout
42+
workflow.GetLogger(ctx).Info("Timeout occurred")
43+
return nil
44+
}
45+
```
46+
47+
#### Resetting the Timer
48+
49+
```go
50+
func MyWorkflow(ctx workflow.Context) error {
51+
timer := resettabletimer.New(ctx, 30*time.Second)
52+
activityChan := workflow.GetSignalChannel(ctx, "activity")
53+
54+
selector := workflow.NewSelector(ctx)
55+
56+
// Add timer to selector
57+
selector.AddFuture(timer.Future, func(f workflow.Future) {
58+
workflow.GetLogger(ctx).Info("User inactive for 30 seconds")
59+
})
60+
61+
// Add signal channel to selector
62+
selector.AddReceive(activityChan, func(c workflow.Channel, more bool) {
63+
var signal string
64+
c.Receive(ctx, &signal)
65+
66+
// Reset the timer when activity is detected
67+
timer.Reset(30 * time.Second)
68+
workflow.GetLogger(ctx).Info("Activity detected, timer reset")
69+
})
70+
71+
selector.Select(ctx)
72+
return nil
73+
}
74+
```
75+
76+
#### Example: Inactivity Timeout with Dynamic Duration
77+
78+
```go
79+
func InactivityTimeoutWorkflow(ctx workflow.Context) error {
80+
// Start with 5 minute timeout
81+
timeout := 5 * time.Minute
82+
timer := resettabletimer.New(ctx, timeout)
83+
84+
activityChan := workflow.GetSignalChannel(ctx, "user_activity")
85+
stopChan := workflow.GetSignalChannel(ctx, "stop")
86+
87+
done := false
88+
for !done {
89+
selector := workflow.NewSelector(ctx)
90+
91+
selector.AddFuture(timer.Future, func(f workflow.Future) {
92+
workflow.GetLogger(ctx).Info("User inactive - logging out")
93+
done = true
94+
})
95+
96+
selector.AddReceive(activityChan, func(c workflow.Channel, more bool) {
97+
var activity struct {
98+
Type string
99+
Timeout time.Duration
100+
}
101+
c.Receive(ctx, &activity)
102+
103+
// Reset with possibly different duration
104+
if activity.Timeout > 0 {
105+
timeout = activity.Timeout
106+
}
107+
timer.Reset(timeout)
108+
109+
workflow.GetLogger(ctx).Info("Activity detected",
110+
"type", activity.Type,
111+
"new_timeout", timeout)
112+
})
113+
114+
selector.AddReceive(stopChan, func(c workflow.Channel, more bool) {
115+
var stop bool
116+
c.Receive(ctx, &stop)
117+
done = true
118+
})
119+
120+
selector.Select(ctx)
121+
}
122+
123+
return nil
124+
}
125+
```
126+
127+
#### API Reference
128+
129+
##### Types
130+
131+
```go
132+
type ResettableTimer interface {
133+
workflow.Future
134+
135+
// Reset cancels the current timer and starts a new one with the given duration.
136+
// If the timer has already fired, Reset has no effect.
137+
Reset(d time.Duration)
138+
}
139+
```
140+
141+
##### Functions
142+
143+
```go
144+
// New creates a new resettable timer that fires after duration d.
145+
func New(ctx workflow.Context, d time.Duration) *ResettableTimer
146+
```
147+
148+
##### Methods
149+
150+
```go
151+
// Reset cancels the current timer and starts a new one with the given duration
152+
timer.Reset(newDuration time.Duration)
153+
154+
// Future is the underlying Future field for use with workflow.Selector
155+
timer.Future workflow.Future
156+
157+
// Get blocks until the timer fires (convenience method)
158+
timer.Future.Get(ctx, nil)
159+
160+
// IsReady returns true if the timer has fired (convenience method)
161+
timer.Future.IsReady()
162+
```
163+
164+
#### Important Notes
165+
166+
1. **Use with Selector**: When using the timer with `workflow.Selector`, you access the Future field directly:
167+
```go
168+
selector.AddFuture(timer.Future, func(f workflow.Future) {
169+
// timer fired
170+
})
171+
```
172+
173+
2. **Reset After Fire**: Once a timer has fired, calling `Reset()` has no effect. The timer is considered "done" after it fires.
174+
175+
3. **Determinism**: Like all workflow code, timer operations are deterministic and will replay correctly during workflow replay.
176+
177+
4. **Resolution**: Timer resolution is in seconds using `math.Ceil(d.Seconds())`, consistent with standard Cadence timers.
178+
179+
#### Testing
180+
181+
The resettable timer works seamlessly with Cadence's workflow test suite:
182+
183+
```go
184+
func TestMyWorkflow(t *testing.T) {
185+
testSuite := &testsuite.WorkflowTestSuite{}
186+
env := testSuite.NewTestWorkflowEnvironment()
187+
188+
// Register delayed callback to simulate activity
189+
env.RegisterDelayedCallback(func() {
190+
env.SignalWorkflow("activity", "user_action")
191+
}, 10*time.Second)
192+
193+
env.ExecuteWorkflow(MyWorkflow)
194+
195+
require.True(t, env.IsWorkflowCompleted())
196+
require.NoError(t, env.GetWorkflowError())
197+
}
198+
```
199+
200+
#### Comparison with Standard Timers
201+
202+
**Standard Timer Pattern:**
203+
```go
204+
// Must manage timer cancellation and recreation manually
205+
var timerCancel workflow.CancelFunc
206+
timerCtx, timerCancel := workflow.WithCancel(ctx)
207+
timer := workflow.NewTimer(timerCtx, 30*time.Second)
208+
209+
// On activity - must cancel and recreate
210+
timerCancel()
211+
timerCtx, timerCancel = workflow.WithCancel(ctx)
212+
timer = workflow.NewTimer(timerCtx, 30*time.Second)
213+
```
214+
215+
**Resettable Timer Pattern:**
216+
```go
217+
// Simple creation and reset
218+
timer := resettabletimer.New(ctx, 30*time.Second)
219+
220+
// On activity - just reset
221+
timer.Reset(30 * time.Second)
222+
```
223+
224+
The resettable timer encapsulates the cancellation and recreation logic, making timeout patterns much cleaner and easier to reason about.
225+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package resettabletimer
2+
3+
import (
4+
"time"
5+
6+
"go.uber.org/cadence"
7+
"go.uber.org/cadence/workflow"
8+
)
9+
10+
type (
11+
// ResettableTimer represents a timer that can be reset to restart its countdown.
12+
ResettableTimer interface {
13+
workflow.Future
14+
15+
// Reset - Cancels the current timer and starts a new one with the given duration.
16+
// If the timer has already fired, Reset has no effect.
17+
Reset(d time.Duration)
18+
}
19+
20+
resettableTimerImpl struct {
21+
ctx workflow.Context
22+
timerCtx workflow.Context
23+
cancelTimer workflow.CancelFunc
24+
// This is suboptimal, but we cannot implement the internal asyncFuture interface because it is not exported. It is what it is.
25+
Future workflow.Future
26+
settable workflow.Settable
27+
duration time.Duration
28+
isReady bool
29+
}
30+
)
31+
32+
// New returns a timer that can be reset to restart its countdown. The timer becomes ready after the
33+
// specified duration d. The timer can be reset using timer.Reset(duration) with a new duration. This is useful for
34+
// implementing timeout patterns that should restart based on external events. The workflow needs to use this
35+
// New() instead of creating new timers repeatedly. The current timer resolution implementation is in
36+
// seconds and uses math.Ceil(d.Seconds()) as the duration. But is subjected to change in the future.
37+
func New(ctx workflow.Context, d time.Duration) *resettableTimerImpl {
38+
rt := &resettableTimerImpl{
39+
ctx: ctx,
40+
duration: d,
41+
}
42+
rt.Future, rt.settable = workflow.NewFuture(ctx)
43+
rt.startTimer(d)
44+
return rt
45+
}
46+
47+
func (rt *resettableTimerImpl) startTimer(d time.Duration) {
48+
rt.duration = d
49+
50+
if rt.cancelTimer != nil {
51+
rt.cancelTimer()
52+
}
53+
54+
rt.timerCtx, rt.cancelTimer = workflow.WithCancel(rt.ctx)
55+
56+
timer := workflow.NewTimer(rt.timerCtx, d)
57+
58+
workflow.Go(rt.ctx, func(ctx workflow.Context) {
59+
err := timer.Get(ctx, nil)
60+
61+
if !cadence.IsCanceledError(err) && !rt.isReady {
62+
rt.isReady = true
63+
rt.settable.Set(nil, err)
64+
}
65+
})
66+
}
67+
68+
// Reset - Cancels the current timer and starts a new one with the given duration.
69+
// If the timer has already fired, Reset has no effect.
70+
func (rt *resettableTimerImpl) Reset(d time.Duration) {
71+
if !rt.isReady {
72+
rt.startTimer(d)
73+
}
74+
}

0 commit comments

Comments
 (0)