# `Lockstep.ETS`
[🔗](https://github.com/b-erdem/lockstep/blob/v0.1.0/lib/lockstep/ets.ex#L1)

Controller-aware wrappers around the most-used `:ets` operations.

ETS itself is BEAM-level shared state -- the operations are atomic
per-row but multiple operations don't compose atomically. The
classic example is read-modify-write:

    v = :ets.lookup_element(t, :counter, 2)
    :ets.insert(t, {:counter, v + 1})

Two processes running this concurrently can both read the same
`v`, then both write `v + 1`, losing one update. Under bare
`:ets`, this race depends on scheduler timing -- rare in tests,
common at scale. With `Lockstep.ETS.lookup_element/3` +
`Lockstep.ETS.insert/2`, every operation is a sync point, so the
scheduler can interleave between A's lookup and A's insert. The
race surfaces on the first iteration that picks that interleaving.

## Usage

Either call `Lockstep.ETS.*` directly, or let `Lockstep.Rewriter`
rewrite `:ets.*` calls in your code under `use Lockstep.Test,
rewrite: true` or via the project-level Mix compiler.

The underlying `:ets` table itself is unchanged -- this module just
inserts a sync point before delegating. Your existing tables are
fully usable.

## Coverage

We wrap the operations most likely to participate in races:

  * `new/2`, `delete/1`, `delete_all_objects/1`
  * `insert/2`, `insert_new/2`
  * `lookup/2`, `lookup_element/3`, `lookup_element/4`
  * `member/2`
  * `delete/2`, `match_delete/2`
  * `update_counter/3`, `update_counter/4`, `update_element/3`
  * `select/2`, `match/2`, `match_object/2`
  * `tab2list/1`, `info/1`, `info/2`
  * `first/1`, `last/1`, `next/2`, `prev/2`

Operations not in this list (e.g., `:ets.give_away/3`) are still
callable as `:ets.give_away(...)` directly -- they just don't yield
to the controller. File an issue if you need one wrapped.

# `delete`

Sync point + `:ets.delete/1`.

# `delete`

Sync point + `:ets.delete/2`.

# `delete_all_objects`

Sync point + `:ets.delete_all_objects/1`.

# `first`

Sync point + `:ets.first/1`.

# `info`

Sync point + `:ets.info/1`.

# `info`

Sync point + `:ets.info/2`.

# `insert`

Sync point + `:ets.insert/2`.

# `insert_new`

Sync point + `:ets.insert_new/2`.

# `last`

Sync point + `:ets.last/1`.

# `lookup`

Sync point + `:ets.lookup/2`.

# `lookup_element`

Sync point + `:ets.lookup_element/3`.

# `lookup_element`

Sync point + `:ets.lookup_element/4`.

# `match`

Sync point + `:ets.match/2`.

# `match_delete`

Sync point + `:ets.match_delete/2`.

# `match_object`

Sync point + `:ets.match_object/2`.

# `member`

Sync point + `:ets.member/2`.

# `new`

Sync point + `:ets.new/2`.

Per-node isolation: when `name` is an atom AND the options request
`:named_table`, the underlying `:ets.new` is called WITHOUT
`:named_table` so BEAM doesn't claim the atom globally. The
resulting tid is registered with the controller against `name` on
the calling process's node, so subsequent lookups via atom name
resolve to the per-node tid. Returns `name` (matching real ETS
named-table behavior) so call sites that store the return value
continue to work.

# `next`

Sync point + `:ets.next/2`.

# `prev`

Sync point + `:ets.prev/2`.

# `safe_fixtable`

Sync point + `:ets.safe_fixtable/2`.

# `select`

Sync point + `:ets.select/1` (continuation form).

# `select`

Sync point + `:ets.select/2`.

# `select`

Sync point + `:ets.select/3`.

# `select_count`

Sync point + `:ets.select_count/2`.

# `tab2list`

Sync point + `:ets.tab2list/1`.

# `take`

Sync point + `:ets.take/2`.

# `update_counter`

Sync point + `:ets.update_counter/3`.

# `update_counter`

Sync point + `:ets.update_counter/4`.

# `update_element`

Sync point + `:ets.update_element/3`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
