NetAuto Academy

Network Controllers

Asynchronous Controller Workflows: Tasks and Polling

Advanced15 min read

What you'll learn

  • Why a controller returns 202 Accepted with a task ID instead of an immediate result
  • How to poll a task until it reaches a final state
  • That a task can be accepted and later fail — a genuinely new failure mode
  • How this extends Phase 3.1's 'HTTP success ≠ data success' into 'accepted ≠ completed'
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

Accepted is not completed

Phase 3.1 taught that a 200 response doesn't guarantee the data is correct. A controller's task workflow adds a sharper version of the same lesson: submitting a change doesn't even guarantee the work is done yet — only that the controller has accepted responsibility for doing it.

  1. Submit desired statePOST /tasks
  2. 202 Accepted + task IDqueued — not done
  3. Poll the taskGET /tasks/{task_id}
  4. runningstill not done
  5. completed or failednow, and only now, is it resolved
Quick checkConcept check
What does a 202 Accepted response from POST /tasks actually guarantee?

Submitting a task, for real

submit_task.py
import requests

response = requests.post(
  "http://127.0.0.1:8766/tasks",
  json={"device_id": "SW2", "type": "vlan_config"},
  headers={"Authorization": "Bearer demo-token"},
  timeout=2,
)
print(response.status_code)
print(response.json())

The Authorization header pattern is identical to Phase 3.1's — not re-taught, just reused. json= sends the desired-state intent as a body, the same requests mechanic from Phase 3.1's mutation lesson.

output
202
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'queued'}

Real, verified output. status_code is 202, not 200 — and the body's own status field says 'queued,' not a result. Both signals agree: this is not a finished answer.

Quick checkIdentify the correct code
After receiving {'task_id': 'task-SW2-1', 'status': 'queued'}, what should the script do next to find out whether the change actually succeeded?

Polling to a real, final state

poll_task.py
import time

def poll_task(task_id):
  for _ in range(10):
      response = requests.get(f"http://127.0.0.1:8766/tasks/{task_id}", timeout=2)
      body = response.json()
      print(body)
      if body["status"] in ("completed", "failed"):
          return body
      time.sleep(0.5)

poll_task("task-SW2-1")

A bounded poll loop — the same 'don't wait forever' discipline as timeout= in Phase 3.1, applied here as a maximum number of polling attempts. time.sleep() between polls is the same pattern Reliable Automation used between retries.

output
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'running'}
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'completed', 'changed': True}

Real, verified output — SW2's task genuinely took two polls to resolve: running, then completed. changed: True means the controller actually made a change, not just confirmed one was already in place.

Quick checkPredict output
Polling a freshly-submitted task for SW1 (which is already fully compliant) returns which sequence of statuses, based on this module's mock controller?

The failure path: accepted, then genuinely failed

poll_a_failing_task.py
response = requests.post(
  "http://127.0.0.1:8766/tasks",
  json={"device_id": "SW4", "type": "vlan_config"},
  headers={"Authorization": "Bearer demo-token"},
  timeout=2,
)
print(response.status_code, response.json())

result = poll_task(response.json()["task_id"])
print(result)

SW4's task is genuinely accepted (202) — the controller has no way to know at submission time that the device won't actually be reachable.

output
202 {'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'queued'}
{'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'running'}
{'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'failed', 'reason': 'device unreachable from controller'}

Real, verified output. The 202 was completely genuine — the controller really did accept the task. The failure only became knowable later, by polling — exactly the gap this lesson exists to close.

A genuinely new failure mode

Every failure Phase 3.1 taught (404, 401, a request timeout) was knowable from the response to the exact request that triggered it. A task's failure is knowable only later, from a separate request (polling) — and can happen for reasons (like a device going unreachable on the controller's southbound side) that have nothing to do with whether the original POST /tasks request itself was well-formed.

Quick checkScenario
A script submits a task, gets 202 Accepted, and — without polling — reports success. SW4's task later fails because the device becomes unreachable. What did the script get wrong?

Key takeaways

  • 202 Accepted with a task ID means the controller accepted the request — not that the change is complete.
  • GET /tasks/{task_id}, polled with a bounded loop and a delay between attempts, is the only way to learn a task's real, final outcome.
  • A task can be genuinely accepted and later genuinely fail — a failure mode with no Phase 3.1 equivalent, since direct REST failures were always knowable immediately.
  • This extends Phase 3.1's 'HTTP success ≠ data success' into 'accepted ≠ completed' — the same discipline, one level further from the original request.

Done with this lesson?

Next lesson

Policies, Templates, and Controller-Driven Change