Lockstep.ETS (Lockstep v0.1.0)

Copy Markdown View Source

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:

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.

Summary

Functions

Sync point + :ets.first/1.

Sync point + :ets.info/1.

Sync point + :ets.last/1.

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

Functions

delete(table)

Sync point + :ets.delete/1.

delete(table, key)

Sync point + :ets.delete/2.

delete_all_objects(table)

Sync point + :ets.delete_all_objects/1.

first(table)

Sync point + :ets.first/1.

info(table)

Sync point + :ets.info/1.

info(table, item)

Sync point + :ets.info/2.

insert(table, object)

Sync point + :ets.insert/2.

insert_new(table, object)

Sync point + :ets.insert_new/2.

last(table)

Sync point + :ets.last/1.

lookup(table, key)

Sync point + :ets.lookup/2.

lookup_element(table, key, pos)

Sync point + :ets.lookup_element/3.

lookup_element(table, key, pos, default)

Sync point + :ets.lookup_element/4.

match(table, pattern)

Sync point + :ets.match/2.

match_delete(table, pattern)

Sync point + :ets.match_delete/2.

match_object(table, pattern)

Sync point + :ets.match_object/2.

member(table, key)

Sync point + :ets.member/2.

new(name, options)

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(table, key)

Sync point + :ets.next/2.

prev(table, key)

Sync point + :ets.prev/2.

safe_fixtable(table, fix)

Sync point + :ets.safe_fixtable/2.

select(continuation)

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

select(table, ms)

Sync point + :ets.select/2.

select(table, ms, limit)

Sync point + :ets.select/3.

select_count(table, ms)

Sync point + :ets.select_count/2.

tab2list(table)

Sync point + :ets.tab2list/1.

take(table, key)

Sync point + :ets.take/2.

update_counter(table, key, op)

Sync point + :ets.update_counter/3.

update_counter(table, key, op, default)

Sync point + :ets.update_counter/4.

update_element(table, key, op)

Sync point + :ets.update_element/3.