# A read-write server

The [read-only tutorial](read-only-server.html) ends where every
interesting question begins: what happens when clients start
writing? This tutorial walks through `Tahr.InMemory` — the
read-write reference backend that ships with the library and powers
`mix tahr.serve` — and explains the protocol obligations that
only show up once state can change.

Try it before reading on:

```bash
mix tahr.serve

# in another terminal
sudo mount -t nfs -o vers=3,tcp,port=2049,mountport=2049 \
  127.0.0.1:/export /mnt/nfs
echo "it works" > /mnt/nfs/proof.txt
ls -l /mnt/nfs
```

## The shape

Three modules:

- **`Tahr.InMemory`** — the store. An `Agent` holding
  `%{tree, next_fileid, writeverf}`, plus every pure helper the
  backends need: handle packing, attribute building, `sattr3`
  application, `wcc_data` capture.
- **`Tahr.InMemory.MountBackend`** — publishes `/export`,
  exactly like the read-only tutorial's version.
- **`Tahr.InMemory.NFSBackend`** — all 21
  `Tahr.NFSv3.Backend` callbacks for real.

`InMemory.programs/0` returns the program registry, so standing the
server up is:

```elixir
:ok = Tahr.InMemory.start()

children = [
  {Tahr.RPC.Server, port: 2049, programs: Tahr.InMemory.programs()}
]
```

## Why an Agent

Every mutation callback in the read-only tutorial was a pure lookup.
Writes force a choice: *who owns the tree, and what stops two
clients racing a rename against a readdir?* The Agent answers both —
`InMemory.update/1` runs each mutation inside
`Agent.get_and_update/3`, so mutations serialise. Reads go through
`InMemory.get/1` and never block a writer for longer than one
callback.

That single decision makes most of the protocol's concurrency
obligations trivial, and it's worth copying: NFSv3's consistency
model assumes operations arrive at the server in *some* order. An
Agent gives you one.

## Weak cache consistency

Open any mutation callback and you'll meet `wcc_data`. NFSv3
clients cache aggressively; when something changes a file or
directory, the server reports the object's attributes from *before*
and *after* the operation so the client can tell "my cache is stale"
from "I made this change myself". Both halves are optional (`nil`),
but a client that never gets them must revalidate constantly.

The pattern in every mutation:

```elixir
pre = InMemory.wcc_pre(node)          # size + mtime + ctime, before
# ...apply the change to the tree...
%WccData{before: pre, after: InMemory.attr(fileid, node)}
```

Errors carry the same envelope — the client needs to refresh its
cache whether the operation landed or not. RENAME is the odd one
out: it moves between two directories, so it reports `wcc_data` for
*both* parents.

## WRITE and COMMIT

WRITE replies state how durable the data is (`:unstable`,
`:data_sync`, or `:file_sync`) alongside a per-boot `writeverf3`.
Clients use the verifier to detect a server restart: if it changes,
uncommitted unstable writes may have been lost and must be resent.

The in-memory backend keeps things honest and simple — every write
goes straight into the tree, so it reports `:file_sync` and COMMIT
has nothing left to flush:

```elixir
reply =
  {:ok,
   %{
     wcc: %WccData{before: pre, after: InMemory.attr(fileid, file)},
     count: byte_size(data),
     committed: :file_sync,
     verf: state.writeverf
   }}
```

The verifier is generated once per boot with
`:crypto.strong_rand_bytes(8)` — a restart really does change it.
A production backend with a write-behind cache would return
`:unstable` or `:data_sync`, track dirty regions, and make COMMIT
actually flush.

## CREATE's three modes

CREATE is one callback with three personalities, selected by a
discriminated union:

- `{:unchecked, sattr}` — create if missing, apply `sattr` if it
  already exists (this is how `O_TRUNC` arrives).
- `{:guarded, sattr}` — fail with `NFS3ERR_EXIST` if the name is
  taken.
- `{:exclusive, verf}` — atomic create-if-not-exists. The subtle
  one: a retried EXCLUSIVE create (client didn't see the reply)
  presents the *same* 8-byte verifier, and must succeed idempotently
  rather than failing with `NFS3ERR_EXIST`. The backend stores the
  verifier on the node at creation and compares on retry:

```elixir
  defp create_existing(state, dir_id, existing_id, {:exclusive, verf}, pre) do
    file = Map.fetch!(state.tree, existing_id)
    wcc = InMemory.wcc(pre, dir_id, state.tree)

    if file.create_verf == verf do
      reply = {:ok, InMemory.handle_for(existing_id), InMemory.attr(existing_id, file), wcc}
      {reply, state}
    else
      {{:error, :exist, wcc}, state}
    end
  end
```

## SETATTR and its guard

`guard_ctime` is the client's optimistic-concurrency check: "apply
these attributes only if the file hasn't changed since I read it."
Compare against the file's `ctime` and refuse with
`NFS3ERR_NOT_SYNC` on mismatch:

```elixir
  defp guard_matches?(_node, nil), do: true
  defp guard_matches?(node, %Nfstime3{} = ctime), do: node.ctime == ctime
```

`InMemory.apply_sattr/3` handles the rest of the `sattr3` contract:
`nil` fields stay unchanged, `:set_to_server_time` and
`{:client, time}` both work, size changes truncate *or* zero-pad,
and `ctime` always advances.

## RENAME's sharp edges

Two worth keeping:

1. **Cycles.** `rename(/a, /a/b)` must fail with `NFS3ERR_INVAL`.
   The backend walks the moved directory's subtree before committing:

   ```elixir
   if node.type == :dir and descendant?(state.tree, fileid, to_id) do
     {from_wcc, to_wcc} = rename_wcc(state.tree, from_dir, from_id, to_dir, to_id)
     {{:error, :inval, from_wcc, to_wcc}, state}
   end
   ```

2. **The existing destination.** Replacing a non-empty directory
   fails with `NFS3ERR_NOTEMPTY`; dropping a file onto a directory
   (or vice versa) fails with `NFS3ERR_ISDIR` / `NFS3ERR_NOTDIR`.
   Otherwise the victim is unlinked first — respecting hard links,
   so a node with `nlink > 1` loses a link rather than its content.

## Cookie verifiers that actually change

The read-only tutorial used a constant `cookieverf3` because its
tree never changes. Here it can't be constant: a client paginating
through READDIR while another client creates files must be told to
restart. `InMemory.verf_for/1` derives the verifier from the
directory's mtime — every mutation bumps the mtime, every stale
pagination gets `NFS3ERR_BAD_COOKIE` and restarts from cookie 0:

```elixir
def verf_for(%{mtime: %Nfstime3{seconds: seconds}}), do: <<seconds::big-unsigned-64>>
```

## Documented limitations

A reference backend stays readable by not pretending to be
production-grade. The module docs carry the full list; the
highlights:

- READDIR/READDIRPLUS ignore the client's byte budgets and return
  everything. Fine for small directories; a production backend must
  stop when the next entry would exceed `maxcount`.
- ACCESS grants everything. A production backend computes the grant
  from the credential's uid/gid and the node's mode.
- MKNOD refuses with `NFS3ERR_NOTSUPP`, which RFC 1813 explicitly
  permits.

## The serve task

`mix tahr.serve` boots `Tahr.InMemory` behind an RPC
listener without any application wiring:

```bash
mix tahr.serve                    # 127.0.0.1:2049
mix tahr.serve --port 0           # ephemeral port, printed on startup
mix tahr.serve --bind 0.0.0.0     # all interfaces
```

The task prints the exact `mount` command for the bound address and
port. See `Mix.Tasks.Tahr.Serve` for the full option list.

## Where next

You've seen every moving part a production backend needs: a state
owner with serialised mutations, wcc capture on every change, a
per-boot write verifier, mtime-derived cookie verifiers, and honest
`nfsstat3` mapping. Everything else — ETS tables, databases, object
stores, clustered filesystems — is the same shape with a different
`tree`.
