import { TaskLifecycle } from '@site/src/components/diagrams/TaskLifecycle';

# Your first distributed task

A **task** is one call to a function decorated with `@eugo.hpc.distribute`.

```python
import eugo.hpc

@eugo.hpc.distribute
def transform(record):
    return {"id": record["id"], "score": record["value"] ** 2}

future = transform({"id": 1, "value": 7})
print(eugo.hpc.get(future))     # {'id': 1, 'score': 49}
```

<TaskLifecycle />

## Tasks are stateless

Each call starts fresh. A task knows nothing about previous calls, and nothing it stores in a global
persists for the next one.

```python
counter = 0

@eugo.hpc.distribute
def broken_increment():
    global counter
    counter += 1        # mutates a COPY on some worker
    return counter

results = eugo.hpc.get([broken_increment() for _ in range(5)])
print(results)          # not [1, 2, 3, 4, 5]
print(counter)          # still 0 in your session
```

Each worker got its own copy of the module state. Nothing propagated back.

That is not a limitation to work around. Statelessness is what makes tasks safely parallelizable. When you
genuinely need state that persists across calls, that is an [actor](./06-actors.mdx).

## What a task can rely on

- **Its arguments.** Serialized from your session, copied to the worker.
- **Its imports.** Anything importable on the compute node.
- **Object storage.** Reachable by every worker.

## What it cannot

- **Your session's variables**, unless passed as arguments.
- **Session-local files** like `/tmp` paths, or workspace-only paths.
- **Mutations reaching your session.** Argument copies are one-way.

## Arguments are copied, so mind the size

```python
big = load_10gb_dataframe()

# Copies 10 GB to a worker, a thousand times over
futures = [process(big, i) for i in range(1000)]
```

If many tasks need the same large object, put it in object storage and have each task read the slice it
needs, or hold it in an [actor](/glossary/actor). Passing it as an argument moves far more data than the computation
saves.

## Return something small

Results travel back over the network too. Returning a 5 GB dataframe per task means gigabytes crossing
the network before you see a value.

Prefer returning a summary, or writing the output to object storage and returning its path:

```python
@eugo.hpc.distribute
def process_partition(path):
    df = read_partition(path)
    out = f"s3://bucket/processed/{basename(path)}"
    df.to_parquet(out)
    return {"path": out, "rows": len(df)}     # small
```

## Exceptions surface at get()

An exception inside the body is raised when you resolve the [future](/glossary/future), with a traceback from inside the
task. Read past the first frame. The useful line number is in the task body, not at the `get()` call.

For finding *which* input failed among many, see
[watching a run](/lesson/eugoide-essentials/monitoring-a-run).

## Task granularity

Dispatching a task costs something. When the body takes milliseconds, that overhead dominates:

```python
# Bad: 1M tasks, each trivial
futures = [add_one(x) for x in range(1_000_000)]

# Better: 1000 tasks, each doing real work
futures = [add_one_batch(chunk) for chunk in chunks(range(1_000_000), 1000)]
```

Aim for task bodies measured in seconds rather than milliseconds.

---

**Video:** [The distribute decorator, explained](/videos/the-distribute-decorator). The same material, with a transcript.

---

Source: https://university.eugo.io/lesson/distributed-python-with-eugo-hpc/your-first-task
