Task lifecycle
This page traces one task from creation to result, then explains how a multi-subtask service-graph request is released and completed. All of it is driven by SimulationManager in pureedgesim/manager.py and DefaultTaskGenerator in pureedgesim/tasks.py.
Generation is eager
The workload is generated in full before the first event runs. start_simulation calls DefaultTaskGenerator.generate(), which:
- Picks the devices that generate tasks (
generates_tasksand alive). - For each application, assigns devices by
usage_percentage(the last application takes the remainder). - Emits tasks per minute. Without an
<arrival>element, a burst ofratetasks is spaced evenly inside each minute; with an arrival distribution, gaps are drawn from it. - For a service-graph application, emits one task per subtask, wires
dependencies/dependents, and stamps every subtask with the samerequest_id,request_task_count,request_start_time, andrequest_deadline. - Sorts all tasks by
time.
The manager then splits the tasks: root_tasks are those with no dependencies. Only roots enter the queue at generation time; every other subtask waits for its parents to return.
The single-task flow
flowchart TD
GEN["Generated by DefaultTaskGenerator"] --> BATCH["Scheduled by batch: SEND_TO_ORCH"]
BATCH --> S2O["_send_to_orchestrator"]
S2O --> F0{"Device alive? (phase 0)"}
F0 -- "dead" --> NOTGEN["not_generated, dependents fail"]
F0 -- "alive" --> ORCHROUTE["Network: device to orchestrator"]
ORCHROUTE --> ODECIDE["Orchestrator picks destination"]
ODECIDE --> F1{"Phase 1: orchestrator alive and in range?"}
F1 -- "no" --> FAILD["death / mobility failure"]
F1 -- "yes" --> F2{"Phase 2: destination alive, resources, in range?"}
F2 -- "no" --> FAILN["death / insufficient resources / mobility"]
F2 -- "yes" --> CONTAINER{"Registry CLOUD and container missing?"}
CONTAINER -- "yes" --> DL["Network: container download"]
CONTAINER -- "no" --> SUBMIT["submit_task at destination"]
DL --> SUBMIT
SUBMIT --> RUN["start_execution: length / mips_per_core"]
RUN --> DONE["EXECUTION_FINISHED"]
DONE --> F3{"Phase 3: deadline met?"}
F3 -- "over latency" --> FAILDELAY["FAILED_DUE_TO_DELAY"]
F3 -- "ok" --> SUCCESS["SUCCESS"]
SUCCESS --> RETURN["Network: results to orchestrator, then device"]
RETURN --> FIN["RESULT_RETURN_FINISHED, dependents released"]
Step by step
SEND_TO_ORCHcalls_send_to_orchestrator. It registers the request, runs the phase-0 check (task_failed(task, 0): if the source device is already dead, the task is counted asnot_generatedand its dependents fail). Then_select_orchestratordecides who decides. Withenable_orchestratorsfalse, that is the device itself (task.get_orchestrator()); otherwise it is chosen fromdeploy_orchestrator(MIST,CLOUD,EDGE, or the nearest node marked as an orchestrator). If the orchestrator is the device, the task goes straight to the destination-decision step; otherwise it is sent over the network to the orchestrator first.SEND_TASK_FROM_ORCH_TO_DESTINATIONcalls_send_from_orchestrator_to_destination. It runs the phase-1 check (orchestrator dead, or the orchestrator is a mist device now out ofedge_devices_range), then finds the destination. If the device hasapplication_placed, it reuses the cachedapplication_placement_location; otherwise it callsorchestrator.orchestrate(task, application_id). ANonedestination is aNO_OFFLOADING_DESTINATIONSfailure. Then the phase-2 check runs (destination alive,has_enough_resources, and within range for a mist destination).- Container download. When
enable_registryis on andregistry_mode == "CLOUD"and the destination is not a cloud node and does not already host the application, the container (task.container_size) is downloaded from the registry over the network first; afterwards the destination id is added toinstalled_containers. submit_tasksetsarrival_timeand either starts the task (if a core and RAM are free) or appends it totask_queue.start_executionrecordsexecution_start_time, charges dynamic energy, computesexecution_duration = length / mips_per_core, and schedulesEXECUTION_FINISHED.EXECUTION_FINISHEDfrees the core and memory, recordsexecution_finish_time, adds the instructions tototal_executed_instructions, and callsmanager.on_task_execution_finished. That increments the executed/execution-time/waiting-time counters and sends the result (result_size) back to the orchestrator, and from there to the source device.RESULT_RETURN_FINISHEDcalls_results_returned, which runs the phase-3 check and, if the task is stillRUNNING, marks itSUCCESS, counts it, releases its dependents, closes the request, and finally gives the orchestrator aresults_returnedcallback.
The four failure checks
task_failed(task, phase) is called at each phase boundary. It returns true (and records the failure) if the task is already terminal.
| Phase | When | Checks | Status set |
|---|---|---|---|
| 0 | Before offloading | Source device not alive | FAILED_DUE_TO_DEVICE_DEATH, counted as not_generated |
| 1 | At the orchestrator | Orchestrator dead, or mist orchestrator out of edge_devices_range |
FAILED_DUE_TO_DEVICE_DEATH / FAILED_DUE_TO_DEVICE_MOBILITY |
| 2 | Before submit | Destination missing/dead, insufficient RAM+storage, mist destination out of edge_data_centers_range |
FAILED_DUE_TO_DEVICE_DEATH / INSUFFICIENT_RESOURCES / FAILED_DUE_TO_DEVICE_MOBILITY |
| 3 | Result returned | total_delay >= max_latency, or request_deadline > 0 and clock >= request_deadline |
FAILED_DUE_TO_DELAY |
A phase-2 “no resources” or a None destination becomes INSUFFICIENT_RESOURCES / NO_OFFLOADING_DESTINATIONS, both counted under not_executed. TaskStatus values are SUCCESS, RUNNING, FAILED_DUE_TO_DELAY, FAILED_DUE_TO_DEVICE_DEATH, FAILED_DUE_TO_DEVICE_MOBILITY, INSUFFICIENT_RESOURCES, NO_OFFLOADING_DESTINATIONS, NULL, FAILED_BECAUSE_DEPENDENCY_FAILED, and SKIPPED.
The task’s own total_delay is total_network_time + waiting_time + actual_cpu_time, exposed as a Task.total_delay property. The request deadline is separate: it is the application latency measured from request_start_time, so a subtask can meet its own bound and still fail because the request as a whole is late.
Service-graph request flow
A request is a set of tasks sharing one request_id. Roots are released at generation; a non-root subtask is released only when every one of its dependencies has returned its results. When a parent returns, its result_size is added to each child’s request_size, so the intermediate data actually crosses the network.
flowchart TD
START["Request created at generation"] --> ROOTS["Root subtasks (no dependencies) released"]
ROOTS --> EXEC["Each subtask runs the single-task flow"]
EXEC --> PARENT{"Parent returned successfully?"}
PARENT -- "yes" --> ADD["Add parent result_size to child request_size"]
ADD --> COUNT{"All dependencies of child returned?"}
COUNT -- "no" --> WAIT["Child keeps waiting"]
COUNT -- "yes" --> RELEASE["Child released: SEND_TO_ORCH now"]
PARENT -- "failed" --> OPT{"Child is optional?"}
OPT -- "yes" --> SKIP["Child SKIPPED; release its own dependents with no results"]
OPT -- "no" --> CFAIL["Child FAILED_BECAUSE_DEPENDENCY_FAILED; recurse to its dependents"]
RELEASE --> EXEC
SKIP --> TERMINAL
CFAIL --> TERMINAL
WAIT --> EXEC
TERMINAL["Child reaches a terminal state"] --> REQDONE{"All subtasks of the request terminal?"}
REQDONE -- "any failed" --> RFAILED["Request failed"]
REQDONE -- "all ok" --> RSUCCESS["Request successful; makespan recorded"]
How the release works
_results_returnedcalls_release_dependents(task)on success. For each dependent that is not already terminal, it addstask.result_sizeto the child’srequest_size, then callschild.dependency_finished(). That method decrementsremaining_dependenciesand returns true once the last one has returned. When it returns true, the child’stimeis set to the current clock and aSEND_TO_ORCHevent is scheduled for it.- On failure,
_fail/phase-0 call_fail_dependents(task). For each non-terminal dependent: an optional child is skipped (_skip_tasksetsSKIPPED, incrementstasks_skipped, and releases its own dependents without carrying results); a required child is markedFAILED_BECAUSE_DEPENDENCY_FAILED, incrementsdependency_failures, and the failure propagates down its subtree.
Request accounting
Each request is a _RequestRecord created on first sight of one of its tasks, which increments generated_requests. _request_task_finished decrements the record’s remaining counter on every terminal subtask. When remaining reaches zero, the request is counted as successful (and its makespan, clock - start_time, accumulated) unless any subtask flagged it failed, in which case it counts as a failed request. These feed the Requests generated, Successful requests, and Average request makespan columns; see Output and metrics.
Termination
A PRINT_LOG event is scheduled at simulation_duration. _check_termination counts tasks that are neither finished nor already failed. If wait_for_all_tasks is true and tasks remain pending while the clock is below simulation_duration + 3600 seconds, it reschedules itself ten seconds later; otherwise it calls engine.terminate(). So wait_for_all_tasks extends the run to drain the queue, bounded by one extra simulated hour.
Cross-links
- The Java lifecycle (same event tags and failure phases): Java Task lifecycle.
- Writing the placement policy that fills in the
orchestratestep: Writing orchestrators.