Skip to content

Commit 4fd5553

Browse files
authored
Merge branch 'proj/rdma-interfaces' into rdma-instance-tests
2 parents 1beb75a + 73f65cf commit 4fd5553

10 files changed

Lines changed: 335 additions & 33 deletions

File tree

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ linters:
3434
- varnamelen
3535
- wrapcheck
3636
- wsl
37+
- gomodguard
38+
- exhaustruct_v5
3739
settings:
3840
dupl:
3941
threshold: 100

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
- Root package files implement the public API client; `k8s/` is a separate helper module for LKE Kubernetes client behavior; `test/` is a separate module for unit and integration tests and replaces both local modules.
66
- API resource files follow a flat root pattern (`instances.go`, `volumes.go`, etc.) and usually pair public types with `Client` methods that call helpers in `request_helpers.go`.
77

8+
## Maintaining This File
9+
- When making changes that invalidate or alter facts documented in this file, update `AGENTS.md` in the same change.
10+
- When reviewing changes, flag anything that may invalidate or alter facts documented in this file if `AGENTS.md` was not updated in the same change.
11+
812
## Commands
913
- Full CI-like local check: `make test` runs build, lint, unit tests, and fixture-backed integration tests; it can be slow because `test-int` uses a 5h timeout.
1014
- Faster focused default: run `go test ./...` at the repo root for root-module unit coverage only, then run focused tests in `test/` or `k8s/` as needed.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ go get -u github.com/linode/linodego/v2
1717

1818
See [godoc](https://godoc.org/github.com/linode/linodego/v2) for a complete reference.
1919

20+
For guidance on waiting for asynchronous API operations, see [Event and Status Polling](docs/event_and_status_polling.md).
21+
2022
The API generally follows the naming patterns prescribed in the [OpenAPIv3 document for Linode APIv4](https://techdocs.akamai.com/linode-api/reference/api).
2123

2224
Deviations in naming have been made to avoid using "Linode" and "Instance" redundantly or inconsistently.

docs/event_and_status_polling.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Event and Status Polling
2+
3+
Many Linode API calls finish on the server after the HTTP response returns. Creating an instance, booting it, resizing a volume, and similar work is asynchronous.
4+
5+
linodego helps you wait for that work in two ways:
6+
7+
1. **Status polling** — keep checking a resource until its status looks right (for example, an instance is `running`).
8+
2. **Event polling** — watch account events until a specific action finishes (for example, `linode_boot` reaches `finished`).
9+
10+
Every wait helper uses the client's poll interval and stops when:
11+
12+
- the condition is met
13+
- an error is returned
14+
- the `context.Context` is canceled or times out
15+
16+
## Quick start: which helper should I use?
17+
18+
| What you want | Use this |
19+
| --- | --- |
20+
| A resource reached a known status | A `WaitFor*Status` helper |
21+
| A specific action finished on an entity | `NewEventPoller` + `WaitForFinished` |
22+
| A create finished, but you did not know the ID yet | `NewEventPollerWithoutEntity` |
23+
| An action on a nested resource (for example, a disk on an instance) | `NewEventPollerWithSecondary` |
24+
| No in-progress events left on a resource | `WaitForResourceFree` |
25+
| A timestamp-based event wait (lower-level alternative) | `WaitForEventFinished` |
26+
27+
**Rule of thumb**
28+
29+
- Use **status waits** when you care whether the resource looks ready.
30+
- Use **event waits** when you care whether a particular action completed.
31+
- Prefer `EventPoller` over `WaitForEventFinished` when you can create the poller before the mutating API call.
32+
33+
## Configuration
34+
35+
### How often does it poll?
36+
37+
By default, the client polls every 3 seconds (`APISecondsPerPoll`).
38+
39+
```go
40+
client.SetPollDelay(5 * time.Second)
41+
delay := client.GetPollDelay()
42+
```
43+
44+
`SetPollDelay` controls how often wait helpers and event pollers check for progress.
45+
46+
By default, request retries also start with a 3 second minimum wait (`SetRetryWaitTime`), matching the poll delay. Those are separate settings: changing the poll delay does not automatically change retry timing.
47+
48+
Shorter poll delays notice completion sooner but create more API traffic. Longer delays are quieter but slower.
49+
50+
### How do timeouts work?
51+
52+
Always pass a deadline-aware context into the wait call itself (`WaitForFinished`, `WaitForInstanceStatus`, and so on). Without a deadline, a wait can block forever if the expected status or event never appears.
53+
54+
```go
55+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
56+
defer cancel()
57+
58+
instance, err := client.WaitForInstanceStatus(ctx, instanceID, linodego.InstanceRunning)
59+
if err != nil {
60+
log.Fatal(err)
61+
}
62+
```
63+
64+
When the context expires, wait helpers return an error that includes `ctx.Err()`.
65+
66+
For `NewEventPoller` and `NewEventPollerWithSecondary`, the create-poller call also takes a context because those helpers list existing events first. For `NewEventPollerWithoutEntity`, only the later wait call needs the deadline context.
67+
68+
## Status polling
69+
70+
Status helpers repeatedly fetch a resource and return once its status matches what you asked for.
71+
72+
### Available helpers
73+
74+
| Method | Waits for |
75+
| --- | --- |
76+
| `WaitForInstanceStatus` | Instance status |
77+
| `WaitForInstanceDiskStatus` | Instance disk status |
78+
| `WaitForVolumeStatus` | Volume status |
79+
| `WaitForVolumeLinodeID` | Volume attach or detach (`LinodeID`) |
80+
| `WaitForVolumeIOReadyStatus` | Volume `IOReady` |
81+
| `WaitForSnapshotStatus` | Instance snapshot status |
82+
| `WaitForImageStatus` | Image status |
83+
| `WaitForImageRegionStatus` | Image replica status in a region |
84+
| `WaitForLKEClusterStatus` | LKE cluster status |
85+
| `WaitForLKEClusterConditions` | Custom LKE conditions |
86+
| `WaitForDatabaseStatus` | Managed database status |
87+
| `WaitForAlertDefinitionStatus` | Monitor alert definition status |
88+
89+
### Example: wait for an instance to become running
90+
91+
```go
92+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
93+
defer cancel()
94+
95+
instance, err := client.CreateInstance(ctx, linodego.InstanceCreateOptions{
96+
Region: "us-east",
97+
Type: "g6-nanode-1",
98+
Label: "polling-example",
99+
Image: "linode/ubuntu22.04",
100+
RootPass: "replace-with-a-secure-password",
101+
})
102+
if err != nil {
103+
log.Fatal(err)
104+
}
105+
106+
instance, err = client.WaitForInstanceStatus(ctx, instance.ID, linodego.InstanceRunning)
107+
if err != nil {
108+
log.Fatal(err)
109+
}
110+
111+
fmt.Printf("instance %d is %s\n", instance.ID, instance.Status)
112+
```
113+
114+
### Example: wait for a volume to become active
115+
116+
```go
117+
volume, err := client.WaitForVolumeStatus(ctx, volumeID, linodego.VolumeActive)
118+
if err != nil {
119+
log.Fatal(err)
120+
}
121+
```
122+
123+
### Example: wait for a volume to attach or detach
124+
125+
```go
126+
// Wait until the volume is attached to this instance.
127+
volume, err := client.WaitForVolumeLinodeID(ctx, volumeID, &instanceID)
128+
if err != nil {
129+
log.Fatal(err)
130+
}
131+
132+
// Wait until the volume is detached.
133+
volume, err = client.WaitForVolumeLinodeID(ctx, volumeID, nil)
134+
if err != nil {
135+
log.Fatal(err)
136+
}
137+
```
138+
139+
## Event polling with EventPoller
140+
141+
Account events describe actions on entities. `EventPoller` watches for one entity and one action.
142+
143+
Create the poller **before** you trigger the operation so the helper can snapshot existing events (record a baseline of event IDs to ignore). That way old events are skipped, and you only wait for the new one.
144+
145+
This is the best option when:
146+
147+
- you care about a specific action completing
148+
- the same entity may have several similar events over time
149+
150+
### Existing entity
151+
152+
```go
153+
// 1. Create the poller first so current events are recorded and ignored.
154+
poller, err := client.NewEventPoller(
155+
ctx,
156+
instance.ID,
157+
linodego.EntityLinode,
158+
linodego.ActionLinodeBoot,
159+
)
160+
if err != nil {
161+
log.Fatal(err)
162+
}
163+
164+
// 2. Trigger the operation.
165+
if err := client.BootInstance(ctx, instance.ID, linodego.InstanceBootOptions{}); err != nil {
166+
log.Fatal(err)
167+
}
168+
169+
// 3. Wait for the matching event to finish.
170+
event, err := poller.WaitForFinished(ctx)
171+
if err != nil {
172+
log.Fatal(err)
173+
}
174+
175+
fmt.Printf("boot event %d finished with status %s\n", event.ID, event.Status)
176+
```
177+
178+
What `WaitForFinished` does:
179+
180+
1. waits for the next unseen matching event
181+
2. polls that event until its status is `finished`
182+
3. returns `nil` and an error if the event status becomes `failed`
183+
184+
If you need the failed event object itself, use `WaitForEventFinished` instead. That helper returns both the event and an error on failure.
185+
186+
If you only need the next matching event, and not necessarily a finished one, call `WaitForLatestUnknownEvent`:
187+
188+
```go
189+
event, err := poller.WaitForLatestUnknownEvent(ctx)
190+
if err != nil {
191+
log.Fatal(err)
192+
}
193+
194+
fmt.Printf("observed event %d with status %s\n", event.ID, event.Status)
195+
```
196+
197+
### Create operations (ID not known yet)
198+
199+
When you create a resource, you usually do not know its ID until the create call returns. Use `NewEventPollerWithoutEntity`, set `EntityID` right after create, then wait.
200+
201+
Unlike `NewEventPoller` and `NewEventPollerWithSecondary`, this helper does **not** snapshot existing events up front. `previousEvents` starts empty. You can create the poller at any time, but you must set `EntityID` before calling `WaitForFinished`.
202+
203+
```go
204+
poller, err := client.NewEventPollerWithoutEntity(
205+
linodego.EntityLinode,
206+
linodego.ActionLinodeCreate,
207+
)
208+
if err != nil {
209+
log.Fatal(err)
210+
}
211+
212+
instance, err := client.CreateInstance(ctx, linodego.InstanceCreateOptions{
213+
Region: "us-east",
214+
Type: "g6-nanode-1",
215+
Label: "create-poll-example",
216+
Booted: linodego.Pointer(false),
217+
})
218+
if err != nil {
219+
log.Fatal(err)
220+
}
221+
222+
// Set this before waiting. Even if create finished quickly, the poller can
223+
// still match the create event because previousEvents starts empty.
224+
poller.EntityID = instance.ID
225+
226+
event, err := poller.WaitForFinished(ctx)
227+
if err != nil {
228+
log.Fatal(err)
229+
}
230+
231+
fmt.Printf("create event %d finished\n", event.ID)
232+
```
233+
234+
### Secondary entities
235+
236+
Some events have both a primary entity and a secondary entity. Deleting a disk on an instance is a common example: the instance is primary, and the disk is secondary.
237+
238+
`NewEventPollerWithSecondary` takes the secondary ID as an `int`, which fits nested resources such as disks.
239+
240+
```go
241+
poller, err := client.NewEventPollerWithSecondary(
242+
ctx,
243+
instance.ID, // primary entity
244+
linodego.EntityLinode,
245+
disk.ID, // secondary entity
246+
linodego.ActionDiskDelete,
247+
)
248+
if err != nil {
249+
log.Fatal(err)
250+
}
251+
252+
if err := client.DeleteInstanceDisk(ctx, instance.ID, disk.ID); err != nil {
253+
log.Fatal(err)
254+
}
255+
256+
event, err := poller.WaitForFinished(ctx)
257+
if err != nil {
258+
log.Fatal(err)
259+
}
260+
```
261+
262+
## Other event helpers
263+
264+
### WaitForEventFinished
265+
266+
`WaitForEventFinished` is a lower-level helper. It finds events matching an entity and action that were created at or after a given timestamp, then waits until one reaches `finished` status.
267+
268+
```go
269+
event, err := client.WaitForEventFinished(
270+
ctx,
271+
instance.ID,
272+
linodego.EntityLinode,
273+
linodego.ActionLinodeCreate,
274+
*instance.Created,
275+
)
276+
if err != nil {
277+
// On failure, event may still be non-nil.
278+
log.Fatal(err)
279+
}
280+
```
281+
282+
Notes:
283+
284+
- Prefer `EventPoller` when you can create the poller before the mutating call. It avoids timestamp edge cases and ignores events that already exist.
285+
- If the matched event fails, this helper returns both the event and an error.
286+
- Entity filtering is optimized for disk, database, linode, domain, and nodebalancer entities. Other entity types may be less precise.
287+
288+
### WaitForResourceFree
289+
290+
Use this when you want a resource to settle before starting another long-running operation. It waits until the entity has no events in `started` or `scheduled` status.
291+
292+
```go
293+
if err := client.WaitForResourceFree(ctx, linodego.EntityLinode, instance.ID); err != nil {
294+
log.Fatal(err)
295+
}
296+
```
297+
298+
## Practical tips
299+
300+
1. **Put the deadline on the wait call.** Status waits, `WaitForFinished`, and `WaitForEventFinished` all need a context that can expire.
301+
2. **Create snapshotting pollers before the mutating call.** `NewEventPoller` and `NewEventPollerWithSecondary` snapshot existing events first. `NewEventPollerWithoutEntity` does not.
302+
3. **Pick the wait that matches your goal.** Status waits answer "is it ready?" Event waits answer "did this action finish?"
303+
4. **Tune the poll delay carefully.** Faster polling is more responsive; slower polling is gentler on the API.
304+
5. **Handle failed events.** `EventPoller.WaitForFinished` returns `nil, error` on failure. `WaitForEventFinished` returns the failed event along with an error.
305+
306+
## Reference
307+
308+
- Implementation: [waitfor.go](../waitfor.go)
309+
- Event, entity, and action constants: [account_events.go](../account_events.go)
310+
- Common event statuses: `EventScheduled`, `EventStarted`, `EventFinished`, `EventFailed`, `EventNotification`, `EventCanceled`

go.mod

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ require (
44
github.com/google/go-cmp v0.7.0
55
github.com/google/go-querystring v1.2.0
66
github.com/jarcoal/httpmock v1.4.2
7-
github.com/stretchr/testify v1.12.0
7+
github.com/stretchr/testify v1.12.1
88
golang.org/x/net v0.58.0
99
golang.org/x/oauth2 v0.36.0
1010
golang.org/x/text v0.41.0
@@ -13,10 +13,7 @@ require (
1313

1414
require (
1515
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
16-
github.com/kr/pretty v0.3.1 // indirect
17-
github.com/rogpeppe/go-internal v1.14.1 // indirect
18-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
19-
gopkg.in/yaml.v3 v3.0.1 // indirect
16+
go.yaml.in/yaml/v3 v3.0.5 // indirect
2017
)
2118

2219
go 1.25.0

0 commit comments

Comments
 (0)