|
| 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` |
0 commit comments