# Task Placement and !Send Execution

Spargio is easiest to use when you treat it as a shard-oriented runtime with explicit placement decisions.

## Core Concepts

- `Shard`: a runtime execution lane with its own scheduling context.
- Placement identifiers: `Pinned(shard)`, `RoundRobin`, `Sticky(key)`,
  `Stealable`, `StealablePreferred(shard)`.
- `Session-local task`: local `!Send` execution pinned to a shard context.
- `Boundary`: a structured cross-runtime/cross-component request-reply channel.

## Default Recommendation: `StealablePreferred`

As a default, use stealable locality-first placement:

- use `spawn_stealable_on(shard, ...)` (equivalent to `StealablePreferred(shard)`)
- default preferred shard: the shard you are currently spawning from
- choose a different preferred shard only when you intentionally route by ownership/session key

Why this is the default:

- you get thread locality for cache-friendly hot paths
- other shards can still steal work when imbalance makes it worthwhile
- migration happens only when Spargio's runtime heuristics decide the expected
  benefit is worth the task migration cost

```rust
use spargio::{RuntimeError, RuntimeHandle, ShardCtx};

#[spargio::main]
async fn main(handle: RuntimeHandle) -> Result<(), RuntimeError> {
    let h = handle.clone();
    let demo = handle.spawn_stealable(async move {
        let current = ShardCtx::current().expect("shard context").shard_id();

        // Default: prefer the shard we are currently spawning from.
        let local_first = h
            .spawn_stealable_on(current, async { "local_first" })
            .expect("spawn");

        // Intentional routing: map an ownership key to a shard.
        let owner_key = 42u64;
        let owner_shard = (owner_key as usize % h.shard_count()) as spargio::ShardId;
        let routed = h
            .spawn_stealable_on(owner_shard, async { "ownership_routed" })
            .expect("spawn");

        let _ = local_first.await.expect("join");
        let _ = routed.await.expect("join");
    })?;

    demo.await?;
    Ok(())
}
```

What this does:

- uses the current spawning shard as the default preferred shard.
- shows routing to another shard only when ownership/session mapping is intentional.

### When to Use Plain `Stealable`

Use plain `Stealable` when your current shard is only an ingress/dispatcher and
is not the ownership home for the work itself.

Example pattern: shard `0` acts as an ingress dispatcher, then submits each
request as plain `spawn_stealable(...)`. In Spargio, `Stealable` submissions
get a round-robin preferred start shard automatically, and can still migrate
later if steal heuristics decide migration is worth the cost.

```rust
use spargio::{net::TcpListener, RuntimeHandle};

#[spargio::main]
async fn main(handle: RuntimeHandle) -> std::io::Result<()> {
    let listener = TcpListener::bind(handle.clone(), "127.0.0.1:7001").await?;
    let dispatcher = handle.clone();

    // Dedicated ingress shard: accept and dispatch.
    let ingress = handle.spawn_pinned(0, async move {
        let mut workers = Vec::new();
        for _ in 0..256 {
            let (stream, _) = listener.accept().await.expect("accept");
            let worker = dispatcher
                .spawn_stealable(async move {
                    let payload = stream.recv(8 * 1024).await.expect("recv");
                    if !payload.is_empty() {
                        stream.write_all(b"ok").await.expect("reply");
                    }
                })
                .expect("spawn_stealable");
            workers.push(worker);
        }

        for worker in workers {
            worker.await.expect("join");
        }
    })?;

    ingress.await?;
    Ok(())
}
```

What this does:

- keeps one shard focused on ingress/dispatch.
- avoids forcing application work to stay near ingress.
- uses `Stealable` to spread initial placement while preserving steal-based
  rebalancing.
- explicitly awaits dispatched tasks so task lifetime is clear in the example.

## Running !Send Work: `run_local_on` and `spawn_local_on`

Use local APIs when your future captures non-`Send` state (for example `Rc`,
`RefCell`, or non-thread-safe FFI handles).

```rust
use std::rc::Rc;

async fn local_entrypoint_demo() -> Result<(), spargio::RuntimeError> {
    let out = spargio::run_local_on(
        spargio::Runtime::builder().shards(2),
        0,
        |_ctx| async move {
            let local_only = Rc::new(String::from("local-state"));
            local_only.len()
        },
    )
    .await?;

    assert_eq!(out, "local-state".len());
    Ok(())
}
```

What this does:

- runs a top-level `!Send` future on one explicit shard.
- allows local-only values (`Rc`) that cannot cross threads.
- keeps the future pinned to that shard for its entire lifetime.

```rust
use spargio::{RuntimeError, RuntimeHandle};
use std::rc::Rc;

#[spargio::main]
async fn main(handle: RuntimeHandle) -> Result<(), RuntimeError> {
    let join = handle.spawn_local_on(0, |_ctx| async move {
        let local_numbers = Rc::new(vec![1u64, 2, 3, 4]);
        local_numbers.iter().sum::<u64>()
    })?;

    assert_eq!(join.await?, 10);
    Ok(())
}
```

What this does:

- spawns one shard-local `!Send` task from an existing runtime handle.
- keeps execution pinned to shard `0` with no migration.
- returns a normal join handle so local work still composes with regular async flows.

## Execution Model

1. You choose an entrypoint (`run`, `run_with`, or `#[spargio::main]`).
2. You choose placement when spawning (for example `spawn_pinned`,
   `spawn_stealable_on`, or `spawn_with_placement(...)`).
3. Task execution location follows placement:
   - `Pinned(shard)`: always runs on the shard you specify.
   - `Sticky(key)`: key is deterministically mapped to one shard; same key stays
     on that shard.
   - `RoundRobin` is spawned on the next shard in a round-robin rotation and
     stays there.
   - `Stealable`: starts as migratable work with no preferred shard.
   - `StealablePreferred(shard)`: starts with a preferred shard for locality, but
     can still migrate if steal heuristics decide migration is worth the cost.
4. I/O can run through high-level fs/net APIs or lower-level native extension
   paths.

## Two Scheduling Goals

- Keep locality when locality is valuable (cache-friendly shard-local state).
- Allow migration when queue imbalance is the bigger cost.

This tradeoff is why placement policy and scheduler tuning both matter.

## Practical Rule of Thumb

- Use `Pinned` when you already know the owning shard.
- Use `Sticky` when you have a stable key (session/user/partition) but do not
  want to manually map shards.
- Use `RoundRobin` for straightforward spread at submission time.
- Default for migratable work: use `spawn_stealable_on(shard, ...)` (equivalent
  to `StealablePreferred(shard)`), usually on the current spawning shard.
- Route to a different preferred shard only when you intentionally align with
  ownership/session locality.
- Use `Stealable` when no meaningful preferred shard exists.
- Tune only after measuring with representative workload shapes.

## Placement APIs in Code

```rust
use spargio::{RuntimeError, RuntimeHandle, ShardCtx, TaskPlacement};

#[spargio::main]
async fn main(handle: RuntimeHandle) -> Result<(), RuntimeError> {
    // Run this demo from inside a shard context.
    let h = handle.clone();
    let demo = handle.spawn_stealable_on(0, async move {
        let current = ShardCtx::current().expect("shard context").shard_id();
        let other = ((usize::from(current) + 1) % h.shard_count()) as spargio::ShardId;

        // Pinned to the current shard.
        let pinned_here = h.spawn_pinned(current, async { "pinned_here" }).expect("spawn");
        // Pinned to a different shard.
        let pinned_other = h.spawn_pinned(other, async { "pinned_other" }).expect("spawn");

        // Spawn on next shard in round-robin rotation.
        let rr = h
            .spawn_with_placement(TaskPlacement::RoundRobin, async { "round_robin" })
            .expect("spawn");
        // Deterministic key-based shard routing.
        let sticky = h
            .spawn_with_placement(TaskPlacement::Sticky(42), async { "sticky" })
            .expect("spawn");
        // Locality-first migratable task (recommended default).
        let preferred_default = h
            .spawn_stealable_on(current, async { "stealable_preferred_default" })
            .expect("spawn");
        // Equivalent explicit placement API form.
        let preferred_explicit = h
            .spawn_with_placement(
                TaskPlacement::StealablePreferred(other),
                async { "stealable_preferred_explicit" },
            )
            .expect("spawn");
        // Migratable with no preferred start shard (fallback when no ownership hint exists).
        let stealable_any = h
            .spawn_with_placement(TaskPlacement::Stealable, async { "stealable_any" })
            .expect("spawn");

        let _ = pinned_here.await.expect("join");
        let _ = pinned_other.await.expect("join");
        let _ = rr.await.expect("join");
        let _ = sticky.await.expect("join");
        let _ = preferred_default.await.expect("join");
        let _ = preferred_explicit.await.expect("join");
        let _ = stealable_any.await.expect("join");
    })?;

    demo.await?;
    Ok(())
}
```

This snippet shows locality-first `StealablePreferred` as the default migratable
choice (via `spawn_stealable_on(...)` and explicit placement), with plain
`Stealable` as the no-preference fallback.
