Skip to the content.

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:

  1. Picks the devices that generate tasks (generates_tasks and alive).
  2. For each application, assigns devices by usage_percentage (the last application takes the remainder).
  3. Emits tasks per minute. Without an <arrival> element, a burst of rate tasks is spaced evenly inside each minute; with an arrival distribution, gaps are drawn from it.
  4. For a service-graph application, emits one task per subtask, wires dependencies/dependents, and stamps every subtask with the same request_id, request_task_count, request_start_time, and request_deadline.
  5. 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

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

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.